Big Change Update

This commit is contained in:
yeongpin
2025-01-14 14:47:41 +08:00
parent 380ea0b81d
commit 19fe4c85f8
651 changed files with 366654 additions and 17 deletions

View File

@@ -0,0 +1,386 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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
*/
/* global CodeMirror, uBlockDashboard */
import './codemirror/ubo-static-filtering.js';
import { dom, qs$ } from './dom.js';
import { i18n$ } from './i18n.js';
import { onBroadcast } from './broadcast.js';
/******************************************************************************/
const cmEditor = new CodeMirror(qs$('#userFilters'), {
autoCloseBrackets: true,
autofocus: true,
extraKeys: {
'Ctrl-Space': 'autocomplete',
'Tab': 'toggleComment',
},
foldGutter: true,
gutters: [
'CodeMirror-linenumbers',
{ className: 'CodeMirror-lintgutter', style: 'width: 11px' },
],
lineNumbers: true,
lineWrapping: true,
matchBrackets: true,
maxScanLines: 1,
styleActiveLine: {
nonEmpty: true,
},
});
uBlockDashboard.patchCodeMirrorEditor(cmEditor);
/******************************************************************************/
// Add auto-complete ability to the editor. Polling is used as the suggested
// hints also depend on the tabs currently opened.
{
let hintUpdateToken = 0;
const getHints = async function() {
const hints = await vAPI.messaging.send('dashboard', {
what: 'getAutoCompleteDetails',
hintUpdateToken
});
if ( hints instanceof Object === false ) { return; }
if ( hints.hintUpdateToken !== undefined ) {
cmEditor.setOption('uboHints', hints);
hintUpdateToken = hints.hintUpdateToken;
}
timer.on(2503);
};
const timer = vAPI.defer.create(( ) => {
getHints();
});
getHints();
}
vAPI.messaging.send('dashboard', {
what: 'getTrustedScriptletTokens',
}).then(tokens => {
cmEditor.setOption('trustedScriptletTokens', tokens);
});
/******************************************************************************/
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`;
}
function setEditorText(text) {
cmEditor.setValue(text.replace(/\s+$/, '') + '\n\n');
}
/******************************************************************************/
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();
}
/******************************************************************************/
// https://github.com/gorhill/uBlock/issues/3704
// Merge changes to user filters occurring in the background with changes
// made in the editor. The code assumes that no deletion occurred in the
// background.
function threeWayMerge(newContent) {
const prvContent = originalState.filters.trim().split(/\n/);
const differ = new self.diff_match_patch();
const newChanges = differ.diff(
prvContent,
newContent.trim().split(/\n/)
);
const usrChanges = differ.diff(
prvContent,
getEditorText().trim().split(/\n/)
);
const out = [];
let i = 0, j = 0, k = 0;
while ( i < prvContent.length ) {
for ( ; j < newChanges.length; j++ ) {
const change = newChanges[j];
if ( change[0] !== 1 ) { break; }
out.push(change[1]);
}
for ( ; k < usrChanges.length; k++ ) {
const change = usrChanges[k];
if ( change[0] !== 1 ) { break; }
out.push(change[1]);
}
if ( k === usrChanges.length || usrChanges[k][0] !== -1 ) {
out.push(prvContent[i]);
}
i += 1; j += 1; k += 1;
}
for ( ; j < newChanges.length; j++ ) {
const change = newChanges[j];
if ( change[0] !== 1 ) { continue; }
out.push(change[1]);
}
for ( ; k < usrChanges.length; k++ ) {
const change = usrChanges[k];
if ( change[0] !== 1 ) { continue; }
out.push(change[1]);
}
return out.join('\n');
}
/******************************************************************************/
async function renderUserFilters(merge = false) {
const details = await vAPI.messaging.send('dashboard', {
what: 'readUserFilters',
});
if ( details instanceof Object === false || details.error ) { return; }
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({ changed: true });
} else {
setEditorText(newContent);
userFiltersChanged({ changed: false });
}
rememberCurrentState();
}
/******************************************************************************/
function handleImportFilePicker(ev) {
const file = ev.target.files[0];
if ( file === undefined || file.name === '' ) { return; }
if ( file.type.indexOf('text') !== 0 ) { return; }
const fr = new FileReader();
fr.onload = function() {
if ( typeof fr.result !== 'string' ) { return; }
const content = uBlockDashboard.mergeNewLines(getEditorText(), fr.result);
cmEditor.operation(( ) => {
const cmPos = cmEditor.getCursor();
setEditorText(content);
cmEditor.setCursor(cmPos);
cmEditor.focus();
});
};
fr.readAsText(file);
}
dom.on('#importFilePicker', 'change', handleImportFilePicker);
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();
}
dom.on('#importUserFiltersFromFile', 'click', startImportFilePicker);
/******************************************************************************/
function exportUserFiltersToFile() {
const val = getEditorText();
if ( val === '' ) { return; }
const filename = i18n$('1pExportFilename')
.replace('{{datetime}}', uBlockDashboard.dateNowToSensibleString())
.replace(/ +/g, '_');
vAPI.download({
'url': `data:text/plain;charset=utf-8,${encodeURIComponent(val)}`,
'filename': filename
});
}
/******************************************************************************/
async function applyChanges() {
const state = getCurrentState();
const details = await vAPI.messaging.send('dashboard', {
what: 'writeUserFilters',
content: state.filters,
enabled: state.enabled,
trusted: state.trusted,
});
if ( details instanceof Object === false || details.error ) { return; }
rememberCurrentState();
userFiltersChanged({ changed: false });
vAPI.messaging.send('dashboard', {
what: 'reloadAllFilters',
});
}
function revertChanges() {
qs$('#enableMyFilters input').checked = originalState.enabled;
qs$('#trustMyFilters input').checked = originalState.trusted;
setEditorText(originalState.filters);
userFiltersChanged();
}
/******************************************************************************/
function getCloudData() {
return getEditorText();
}
function setCloudData(data, append) {
if ( typeof data !== 'string' ) { return; }
if ( append ) {
data = uBlockDashboard.mergeNewLines(getEditorText(), data);
}
cmEditor.setValue(data);
}
self.cloud.onPush = getCloudData;
self.cloud.onPull = setCloudData;
/******************************************************************************/
self.wikilink = 'https://github.com/gorhill/uBlock/wiki/Dashboard:-My-filters';
self.hasUnsavedData = function() {
return currentStateChanged();
};
/******************************************************************************/
// Handle user interaction
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();
cmEditor.clearHistory();
// https://github.com/gorhill/uBlock/issues/3706
// Save/restore cursor position
{
const line = await vAPI.localStorage.getItemAsync('myFiltersCursorPosition');
if ( typeof line === 'number' ) {
cmEditor.setCursor(line, 0);
}
cmEditor.focus();
}
// https://github.com/gorhill/uBlock/issues/3706
// Save/restore cursor position
{
let curline = 0;
cmEditor.on('cursorActivity', ( ) => {
if ( timer.ongoing() ) { return; }
if ( cmEditor.getCursor().line === curline ) { return; }
timer.on(701);
});
const timer = vAPI.defer.create(( ) => {
curline = cmEditor.getCursor().line;
vAPI.localStorage.setItem('myFiltersCursorPosition', curline);
});
}
// https://github.com/gorhill/uBlock/issues/3704
// Merge changes to user filters occurring in the background
onBroadcast(msg => {
switch ( msg.what ) {
case 'userFiltersUpdated': {
cmEditor.startOperation();
const scroll = cmEditor.getScrollInfo();
const selections = cmEditor.listSelections();
renderUserFilters(true).then(( ) => {
cmEditor.clearHistory();
cmEditor.setSelection(selections[0].anchor, selections[0].head);
cmEditor.scrollTo(scroll.left, scroll.top);
cmEditor.endOperation();
});
break;
}
default:
break;
}
});
})();
cmEditor.on('changes', userFiltersChanged);
CodeMirror.commands.save = applyChanges;
/******************************************************************************/

View File

@@ -0,0 +1,906 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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
*/
import { dom, qs$, qsa$ } from './dom.js';
import { i18n, i18n$ } from './i18n.js';
import { onBroadcast } from './broadcast.js';
/******************************************************************************/
const lastUpdateTemplateString = i18n$('3pLastUpdate');
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 = {};
/******************************************************************************/
onBroadcast(msg => {
switch ( msg.what ) {
case 'assetUpdated':
updateAssetStatus(msg);
break;
case 'assetsUpdated':
dom.cl.remove(dom.body, 'updating');
renderWidgets();
break;
case 'staticFilteringDataChanged':
renderFilterLists();
break;
default:
break;
}
});
/******************************************************************************/
const renderNumber = value => {
return value.toLocaleString();
};
const listStatsTemplate = i18n$('3pListsOfBlockedHostsPerListStats');
const renderLeafStats = (used, total) => {
if ( isNaN(used) || isNaN(total) ) { return ''; }
return listStatsTemplate
.replace('{{used}}', renderNumber(used))
.replace('{{total}}', renderNumber(total));
};
const renderNodeStats = (used, total) => {
if ( isNaN(used) || isNaN(total) ) { return ''; }
return `${used.toLocaleString()}/${total.toLocaleString()}`;
};
const i18nGroupName = name => {
const groupname = i18n$('3pGroup' + name.charAt(0).toUpperCase() + name.slice(1));
if ( groupname !== '' ) { return groupname; }
return `${name.charAt(0).toLocaleUpperCase}${name.slice(1)}`;
};
/******************************************************************************/
const renderFilterLists = ( ) => {
// Assemble a pretty list name if possible
const listNameFromListKey = listkey => {
const list = listsetDetails.current[listkey] || listsetDetails.available[listkey];
const title = list && list.title || '';
if ( title !== '' ) { return title; }
return listkey;
};
const initializeListEntry = (listDetails, listEntry) => {
const listkey = listEntry.dataset.key;
const groupkey = listDetails.group2 || listDetails.group;
const listEntryPrevious =
qs$(`[data-key="${groupkey}"] [data-key="${listkey}"]`);
if ( listEntryPrevious !== null ) {
if ( dom.cl.has(listEntryPrevious, 'checked') ) {
dom.cl.add(listEntry, 'checked');
}
if ( dom.cl.has(listEntryPrevious, 'stickied') ) {
dom.cl.add(listEntry, 'stickied');
}
if ( dom.cl.has(listEntryPrevious, 'toRemove') ) {
dom.cl.add(listEntry, 'toRemove');
}
if ( dom.cl.has(listEntryPrevious, 'searchMatch') ) {
dom.cl.add(listEntry, 'searchMatch');
}
} else {
dom.cl.toggle(listEntry, 'checked', listDetails.off !== true);
}
const on = dom.cl.has(listEntry, 'checked');
dom.prop(qs$(listEntry, ':scope > .detailbar input'), 'checked', on);
let elem = qs$(listEntry, ':scope > .detailbar a.content');
dom.attr(elem, 'href', 'asset-viewer.html?url=' + encodeURIComponent(listkey));
dom.attr(elem, 'type', 'text/html');
dom.cl.remove(listEntry, 'toRemove');
if ( listDetails.supportName ) {
elem = qs$(listEntry, ':scope > .detailbar a.support');
dom.attr(elem, 'href', listDetails.supportURL || '#');
dom.attr(elem, 'title', listDetails.supportName);
}
if ( listDetails.external ) {
dom.cl.add(listEntry, 'external');
} else {
dom.cl.remove(listEntry, 'external');
}
if ( listDetails.instructionURL ) {
elem = qs$(listEntry, ':scope > .detailbar a.mustread');
dom.attr(elem, 'href', listDetails.instructionURL || '#');
}
dom.cl.toggle(listEntry, 'isDefault',
listDetails.isDefault === true ||
listDetails.isImportant === true ||
listkey === 'user-filters'
);
elem = qs$(listEntry, '.leafstats');
dom.text(elem, renderLeafStats(on ? listDetails.entryUsedCount : 0, listDetails.entryCount));
// https://github.com/chrisaljoudi/uBlock/issues/104
const asset = listsetDetails.cache[listkey] || {};
const remoteURL = asset.remoteURL;
dom.cl.toggle(listEntry, 'unsecure',
typeof remoteURL === 'string' && remoteURL.lastIndexOf('http:', 0) === 0
);
dom.cl.toggle(listEntry, 'failed', asset.error !== undefined);
dom.cl.toggle(listEntry, 'obsolete', asset.obsolete === true);
const lastUpdateString = lastUpdateTemplateString.replace('{{ago}}',
i18n.renderElapsedTimeToString(asset.writeTime || 0)
);
if ( asset.obsolete === true ) {
let title = obsoleteTemplateString;
if ( asset.cached && asset.writeTime !== 0 ) {
title += '\n' + lastUpdateString;
}
dom.attr(qs$(listEntry, ':scope > .detailbar .status.obsolete'), 'title', title);
}
if ( asset.cached === true ) {
dom.cl.add(listEntry, 'cached');
dom.attr(qs$(listEntry, ':scope > .detailbar .status.cache'), 'title', lastUpdateString);
const timeSinceLastUpdate = Date.now() - asset.writeTime;
dom.cl.toggle(listEntry, 'recent', timeSinceLastUpdate < recentlyUpdated);
} else {
dom.cl.remove(listEntry, 'cached');
}
};
const createListEntry = (listDetails, depth) => {
if ( listDetails.lists === undefined ) {
return dom.clone('#templates .listEntry[data-role="leaf"]');
}
if ( depth !== 0 ) {
return dom.clone('#templates .listEntry[data-role="node"]');
}
return dom.clone('#templates .listEntry[data-role="node"][data-parent="root"]');
};
const createListEntries = (parentkey, listTree, depth = 0) => {
const listEntries = dom.clone('#templates .listEntries');
const treeEntries = Object.entries(listTree);
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);
});
}
for ( const [ listkey, listDetails ] of treeEntries ) {
const listEntry = createListEntry(listDetails, depth);
if ( dom.cl.has(dom.root, 'mobile') ) {
const leafStats = qs$(listEntry, '.leafstats');
if ( leafStats ) {
listEntry.append(leafStats);
}
}
listEntry.dataset.key = listkey;
listEntry.dataset.parent = parentkey;
qs$(listEntry, ':scope > .detailbar .listname').append(
i18n.patchUnicodeFlags(listDetails.title)
);
if ( listDetails.lists !== undefined ) {
listEntry.append(createListEntries(listEntry.dataset.key, listDetails.lists, depth+1));
dom.cl.toggle(listEntry, 'expanded', listIsExpanded(listkey));
updateListNode(listEntry);
} else {
initializeListEntry(listDetails, listEntry);
}
listEntries.append(listEntry);
}
return listEntries;
};
const onListsReceived = response => {
// Store in global variable
listsetDetails = response;
hashFromListsetDetails();
// Build list tree
const listTree = {};
const groupKeys = [
'user',
'default',
'ads',
'privacy',
'malware',
'multipurpose',
'cookies',
'social',
'annoyances',
'regions',
'unknown',
'custom'
];
for ( const key of groupKeys ) {
listTree[key] = {
title: i18nGroupName(key),
lists: {},
};
}
for ( const [ listkey, listDetails ] of Object.entries(response.available) ) {
let groupkey = listDetails.group2 || listDetails.group;
if ( hasOwnProperty(listTree, groupkey) === false ) {
groupkey = 'unknown';
}
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;
} else {
listDetails.title = listNameFromListKey(listkey);
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);
qs$('#autoUpdate').checked = listsetDetails.autoUpdate === true;
dom.text(
'#listsOfBlockedHostsPrompt',
i18n$('3pListsOfBlockedHostsPrompt')
.replace('{{netFilterCount}}', renderNumber(response.netFilterCount))
.replace('{{cosmeticFilterCount}}', renderNumber(response.cosmeticFilterCount))
);
qs$('#parseCosmeticFilters').checked =
listsetDetails.parseCosmeticFilters === true;
qs$('#ignoreGenericCosmeticFilters').checked =
listsetDetails.ignoreGenericCosmeticFilters === true;
qs$('#suspendUntilListsAreLoaded').checked =
listsetDetails.suspendUntilListsAreLoaded === true;
// https://github.com/gorhill/uBlock/issues/2394
dom.cl.toggle(dom.body, 'updating', listsetDetails.isUpdating);
renderWidgets();
};
return vAPI.messaging.send('dashboard', {
what: 'getLists',
}).then(response => {
onListsReceived(response);
});
};
/******************************************************************************/
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()
);
dom.cl.toggle('#buttonUpdate', 'active', updating);
dom.cl.toggle('#buttonUpdate', 'disabled',
updating === false && hasObsolete === false
);
};
/******************************************************************************/
const updateAssetStatus = details => {
const listEntry = qs$(`#lists .listEntry[data-key="${details.key}"]`);
if ( listEntry === null ) { return; }
dom.cl.toggle(listEntry, 'failed', !!details.failed);
dom.cl.toggle(listEntry, 'obsolete', !details.cached);
dom.cl.toggle(listEntry, 'cached', !!details.cached);
if ( details.cached ) {
dom.attr(qs$(listEntry, '.status.cache'), 'title',
lastUpdateTemplateString.replace('{{ago}}', i18n.renderElapsedTimeToString(Date.now()))
);
dom.cl.add(listEntry, 'recent');
}
updateAncestorListNodes(listEntry, ancestor => {
updateListNode(ancestor);
});
renderWidgets();
};
/*******************************************************************************
Compute a hash from all the settings affecting how filter lists are loaded
in memory.
**/
let filteringSettingsHash = '';
const hashFromListsetDetails = ( ) => {
const hashParts = [
listsetDetails.parseCosmeticFilters === true,
listsetDetails.ignoreGenericCosmeticFilters === true,
];
const listHashes = [];
for ( const [ listkey, listDetails ] of Object.entries(listsetDetails.available) ) {
if ( listDetails.off === true ) { continue; }
listHashes.push(listkey);
}
hashParts.push( listHashes.sort().join(), '', false);
filteringSettingsHash = hashParts.join();
};
const hashFromCurrentFromSettings = ( ) => {
const hashParts = [
qs$('#parseCosmeticFilters').checked,
qs$('#ignoreGenericCosmeticFilters').checked,
];
const listHashes = [];
const listEntries = qsa$('#lists .listEntry[data-key]:not(.toRemove)');
for ( const liEntry of listEntries ) {
if ( liEntry.dataset.role !== 'leaf' ) { continue; }
if ( dom.cl.has(liEntry, 'checked') === false ) { continue; }
listHashes.push(liEntry.dataset.key);
}
const textarea = qs$('#lists .listEntry[data-role="import"].expanded textarea');
hashParts.push(
listHashes.sort().join(),
textarea !== null && textarea.value.trim() || '',
qs$('#lists .listEntry.toRemove') !== null
);
return hashParts.join();
};
/******************************************************************************/
const onListsetChanged = ev => {
const input = ev.target.closest('input');
if ( input === null ) { return; }
toggleFilterList(input, input.checked, true);
};
dom.on('#lists', 'change', '.listEntry > .detailbar input', onListsetChanged);
const toggleFilterList = (elem, on, ui = false) => {
const listEntry = elem.closest('.listEntry');
if ( listEntry === null ) { return; }
if ( listEntry.dataset.parent === 'root' ) { return; }
const searchMode = dom.cl.has('#lists', 'searchMode');
const input = qs$(listEntry, ':scope > .detailbar input');
if ( on === undefined ) {
on = input.checked === false;
}
input.checked = on;
dom.cl.toggle(listEntry, 'checked', on);
dom.cl.toggle(listEntry, 'stickied', ui && !on && !searchMode);
// Select/unselect descendants. Twist: if in search-mode, select only
// search-matched descendants.
const childListEntries = searchMode
? qsa$(listEntry, '.listEntry.searchMatch')
: qsa$(listEntry, '.listEntry');
for ( const descendantList of childListEntries ) {
dom.cl.toggle(descendantList, 'checked', on);
qs$(descendantList, ':scope > .detailbar input').checked = on;
}
updateAncestorListNodes(listEntry, ancestor => {
updateListNode(ancestor);
});
onFilteringSettingsChanged();
};
const updateListNode = listNode => {
if ( listNode === null ) { return; }
if ( listNode.dataset.role !== 'node' ) { return; }
const checkedListLeaves = qsa$(listNode, '.listEntry[data-role="leaf"].checked');
const allListLeaves = qsa$(listNode, '.listEntry[data-role="leaf"]');
dom.text(qs$(listNode, '.nodestats'),
renderNodeStats(checkedListLeaves.length, allListLeaves.length)
);
dom.cl.toggle(listNode, 'searchMatch',
qs$(listNode, ':scope > .listEntries > .listEntry.searchMatch') !== null
);
if ( listNode.dataset.parent === 'root' ) { return; }
let usedFilterCount = 0;
let totalFilterCount = 0;
let isCached = false;
let isObsolete = false;
let latestWriteTime = 0;
let oldestWriteTime = Number.MAX_SAFE_INTEGER;
for ( const listLeaf of checkedListLeaves ) {
const listkey = listLeaf.dataset.key;
const listDetails = listsetDetails.available[listkey];
usedFilterCount += listDetails.off ? 0 : listDetails.entryUsedCount || 0;
totalFilterCount += listDetails.entryCount || 0;
const assetCache = listsetDetails.cache[listkey] || {};
isCached = isCached || dom.cl.has(listLeaf, 'cached');
isObsolete = isObsolete || dom.cl.has(listLeaf, 'obsolete');
latestWriteTime = Math.max(latestWriteTime, assetCache.writeTime || 0);
oldestWriteTime = Math.min(oldestWriteTime, assetCache.writeTime || Number.MAX_SAFE_INTEGER);
}
dom.cl.toggle(listNode, 'checked', checkedListLeaves.length !== 0);
dom.cl.toggle(qs$(listNode, ':scope > .detailbar .checkbox'),
'partial',
checkedListLeaves.length !== allListLeaves.length
);
dom.prop(qs$(listNode, ':scope > .detailbar input'),
'checked',
checkedListLeaves.length !== 0
);
dom.text(qs$(listNode, '.leafstats'),
renderLeafStats(usedFilterCount, totalFilterCount)
);
const firstLeaf = qs$(listNode, '.listEntry[data-role="leaf"]');
if ( firstLeaf !== null ) {
dom.attr(qs$(listNode, ':scope > .detailbar a.support'), 'href',
dom.attr(qs$(firstLeaf, ':scope > .detailbar a.support'), 'href') || '#'
);
dom.attr(qs$(listNode, ':scope > .detailbar a.mustread'), 'href',
dom.attr(qs$(firstLeaf, ':scope > .detailbar a.mustread'), 'href') || '#'
);
}
dom.cl.toggle(listNode, 'cached', isCached);
dom.cl.toggle(listNode, 'obsolete', isObsolete);
if ( isCached ) {
dom.attr(qs$(listNode, ':scope > .detailbar .cache'), 'title',
lastUpdateTemplateString.replace('{{ago}}', i18n.renderElapsedTimeToString(latestWriteTime))
);
dom.cl.toggle(listNode, 'recent', (Date.now() - oldestWriteTime) < recentlyUpdated);
}
if ( qs$(listNode, '.listEntry.isDefault') !== null ) {
dom.cl.add(listNode, 'isDefault');
}
if ( qs$(listNode, '.listEntry.stickied') !== null ) {
dom.cl.add(listNode, 'stickied');
}
};
const updateAncestorListNodes = (listEntry, fn) => {
while ( listEntry !== null ) {
fn(listEntry);
listEntry = qs$(`.listEntry[data-key="${listEntry.dataset.parent}"]`);
}
};
/******************************************************************************/
const onFilteringSettingsChanged = ( ) => {
renderWidgets();
};
dom.on('#parseCosmeticFilters', 'change', onFilteringSettingsChanged);
dom.on('#ignoreGenericCosmeticFilters', 'change', onFilteringSettingsChanged);
dom.on('#lists', 'input', '[data-role="import"] textarea', onFilteringSettingsChanged);
/******************************************************************************/
const onRemoveExternalList = ev => {
const listEntry = ev.target.closest('[data-key]');
if ( listEntry === null ) { return; }
dom.cl.toggle(listEntry, 'toRemove');
renderWidgets();
};
dom.on('#lists', 'click', '.listEntry .remove', onRemoveExternalList);
/******************************************************************************/
const onPurgeClicked = ev => {
const liEntry = ev.target.closest('[data-key]');
const listkey = liEntry.dataset.key || '';
if ( listkey === '' ) { return; }
const assetKeys = [ listkey ];
for ( const listLeaf of qsa$(liEntry, '[data-role="leaf"]') ) {
assetKeys.push(listLeaf.dataset.key);
dom.cl.add(listLeaf, 'obsolete');
dom.cl.remove(listLeaf, 'cached');
}
vAPI.messaging.send('dashboard', {
what: 'listsUpdateNow',
assetKeys,
preferOrigin: ev.shiftKey,
});
// If the cached version is purged, the installed version must be assumed
// to be obsolete.
// https://github.com/gorhill/uBlock/issues/1733
// An external filter list must not be marked as obsolete, they will
// always be fetched anyways if there is no cached copy.
dom.cl.add(dom.body, 'updating');
dom.cl.add(liEntry, 'obsolete');
if ( qs$(liEntry, 'input[type="checkbox"]').checked ) {
renderWidgets();
}
};
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+/);
const after = [];
for ( const line of lines ) {
after.push(line);
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 ) { continue; }
const groupkey = list.group2 || list.group;
const listEntry = qs$(`[data-key="${groupkey}"] [data-key="${listkey}"]`);
if ( listEntry === null ) { break; }
toggleFilterList(listEntry, true);
after.pop();
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', {
what: 'userSettings',
name: 'parseAllABPHideFilters',
value: checked,
});
listsetDetails.parseCosmeticFilters = checked;
checked = qs$('#ignoreGenericCosmeticFilters').checked;
vAPI.messaging.send('dashboard', {
what: 'userSettings',
name: 'ignoreGenericCosmeticFilters',
value: checked,
});
listsetDetails.ignoreGenericCosmeticFilters = checked;
// Filter lists to remove/select
const toSelect = [];
const toRemove = [];
for ( const liEntry of qsa$('#lists .listEntry[data-role="leaf"]') ) {
const listkey = liEntry.dataset.key;
if ( hasOwnProperty(listsetDetails.available, listkey) === false ) {
continue;
}
const listDetails = listsetDetails.available[listkey];
if ( dom.cl.has(liEntry, 'toRemove') ) {
toRemove.push(listkey);
listDetails.off = true;
continue;
}
if ( dom.cl.has(liEntry, 'checked') ) {
toSelect.push(listkey);
listDetails.off = false;
} else {
listDetails.off = true;
}
}
hashFromListsetDetails();
await vAPI.messaging.send('dashboard', {
what: 'applyFilterListSelection',
toSelect,
toImport,
toRemove,
});
};
/******************************************************************************/
const buttonApplyHandler = async ( ) => {
await selectFilterLists();
dom.cl.add(dom.body, 'working');
dom.cl.remove('#lists .listEntry.stickied', 'stickied');
renderWidgets();
await vAPI.messaging.send('dashboard', { what: 'reloadAllFilters' });
dom.cl.remove(dom.body, 'working');
};
dom.on('#buttonApply', 'click', ( ) => { buttonApplyHandler(); });
/******************************************************************************/
const buttonUpdateHandler = async ( ) => {
dom.cl.remove('#lists .listEntry.stickied', 'stickied');
await selectFilterLists();
dom.cl.add(dom.body, 'updating');
renderWidgets();
vAPI.messaging.send('dashboard', { what: 'updateNow' });
};
dom.on('#buttonUpdate', 'click', ( ) => { buttonUpdateHandler(); });
/******************************************************************************/
const userSettingCheckboxChanged = ( ) => {
const target = event.target;
vAPI.messaging.send('dashboard', {
what: 'userSettings',
name: target.id,
value: target.checked,
});
listsetDetails[target.id] = target.checked;
};
dom.on('#autoUpdate', 'change', userSettingCheckboxChanged);
dom.on('#suspendUntilListsAreLoaded', 'change', userSettingCheckboxChanged);
/******************************************************************************/
const searchFilterLists = ( ) => {
const pattern = dom.prop('.searchfield input', 'value') || '';
dom.cl.toggle('#lists', 'searchMode', pattern !== '');
if ( pattern === '' ) { return; }
const reflectSearchMatches = listEntry => {
if ( listEntry.dataset.role !== 'node' ) { return; }
dom.cl.toggle(listEntry, 'searchMatch',
qs$(listEntry, ':scope > .listEntries > .listEntry.searchMatch') !== null
);
};
const toI18n = tags => {
if ( tags === '' ) { return ''; }
return tags.toLowerCase().split(/\s+/).reduce((a, v) => {
let s = i18n$(v);
if ( s === '' ) {
s = i18nGroupName(v);
if ( s === '' ) { return a; }
}
return `${a} ${s}`.trim();
}, '');
};
const re = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
for ( const listEntry of qsa$('#lists [data-role="leaf"]') ) {
const listkey = listEntry.dataset.key;
const listDetails = listsetDetails.available[listkey];
if ( listDetails === undefined ) { continue; }
let haystack = perListHaystack.get(listDetails);
if ( haystack === undefined ) {
const groupkey = listDetails.group2 || listDetails.group || '';
haystack = [
listDetails.title,
groupkey,
i18nGroupName(groupkey),
listDetails.tags || '',
toI18n(listDetails.tags || ''),
].join(' ').trim();
perListHaystack.set(listDetails, haystack);
}
dom.cl.toggle(listEntry, 'searchMatch', re.test(haystack));
updateAncestorListNodes(listEntry, reflectSearchMatches);
}
};
const perListHaystack = new WeakMap();
dom.on('.searchfield input', 'input', searchFilterLists);
/******************************************************************************/
const expandedListSet = new Set([
'cookies',
'social',
]);
const listIsExpanded = which => {
return expandedListSet.has(which);
};
const applyListExpansion = listkeys => {
if ( listkeys === undefined ) {
listkeys = Array.from(expandedListSet);
}
expandedListSet.clear();
dom.cl.remove('#lists [data-role="node"]', 'expanded');
listkeys.forEach(which => {
expandedListSet.add(which);
dom.cl.add(`#lists [data-key="${which}"]`, 'expanded');
});
};
const toggleListExpansion = which => {
const isExpanded = expandedListSet.has(which);
if ( which === '*' ) {
if ( isExpanded ) {
expandedListSet.clear();
dom.cl.remove('#lists .expandable', 'expanded');
dom.cl.remove('#lists .stickied', 'stickied');
} else {
expandedListSet.clear();
expandedListSet.add('*');
dom.cl.add('#lists .rootstats', 'expanded');
for ( const expandable of qsa$('#lists > .listEntries .expandable') ) {
const listkey = expandable.dataset.key || '';
if ( listkey === '' ) { continue; }
expandedListSet.add(listkey);
dom.cl.add(expandable, 'expanded');
}
}
} else {
if ( isExpanded ) {
expandedListSet.delete(which);
const listNode = qs$(`#lists > .listEntries [data-key="${which}"]`);
dom.cl.remove(listNode, 'expanded');
if ( listNode.dataset.parent === 'root' ) {
dom.cl.remove(qsa$(listNode, '.stickied'), 'stickied');
}
} else {
expandedListSet.add(which);
dom.cl.add(`#lists > .listEntries [data-key="${which}"]`, 'expanded');
}
}
vAPI.localStorage.setItem('expandedListSet', Array.from(expandedListSet));
vAPI.localStorage.removeItem('hideUnusedFilterLists');
};
dom.on('#listsOfBlockedHostsPrompt', 'click', ( ) => {
toggleListExpansion('*');
});
dom.on('#lists', 'click', '.listExpander', ev => {
const expandable = ev.target.closest('.expandable');
if ( expandable === null ) { return; }
const which = expandable.dataset.key;
if ( which !== undefined ) {
toggleListExpansion(which);
} else {
dom.cl.toggle(expandable, 'expanded');
if ( expandable.dataset.role === 'import' ) {
onFilteringSettingsChanged();
}
}
ev.preventDefault();
});
dom.on('#lists', 'click', '[data-parent="root"] > .detailbar .listname', ev => {
const listEntry = ev.target.closest('.listEntry');
if ( listEntry === null ) { return; }
const listkey = listEntry.dataset.key;
if ( listkey === undefined ) { return; }
toggleListExpansion(listkey);
ev.preventDefault();
});
dom.on('#lists', 'click', '[data-role="import"] > .detailbar .listname', ev => {
const expandable = ev.target.closest('.listEntry');
if ( expandable === null ) { return; }
dom.cl.toggle(expandable, 'expanded');
ev.preventDefault();
});
dom.on('#lists', 'click', '.listEntry > .detailbar .nodestats', ev => {
const listEntry = ev.target.closest('.listEntry');
if ( listEntry === null ) { return; }
const listkey = listEntry.dataset.key;
if ( listkey === undefined ) { return; }
toggleListExpansion(listkey);
ev.preventDefault();
});
// Initialize from saved state.
vAPI.localStorage.getItemAsync('expandedListSet').then(listkeys => {
if ( Array.isArray(listkeys) === false ) { return; }
applyListExpansion(listkeys);
});
/******************************************************************************/
// Cloud storage-related.
self.cloud.onPush = function toCloudData() {
const bin = {
parseCosmeticFilters: qs$('#parseCosmeticFilters').checked,
ignoreGenericCosmeticFilters: qs$('#ignoreGenericCosmeticFilters').checked,
selectedLists: []
};
const liEntries = qsa$('#lists .listEntry.checked[data-role="leaf"]');
for ( const liEntry of liEntries ) {
bin.selectedLists.push(liEntry.dataset.key);
}
return bin;
};
self.cloud.onPull = function fromCloudData(data, append) {
if ( typeof data !== 'object' || data === null ) { return; }
let elem = qs$('#parseCosmeticFilters');
let checked = data.parseCosmeticFilters === true || append && elem.checked;
elem.checked = listsetDetails.parseCosmeticFilters = checked;
elem = qs$('#ignoreGenericCosmeticFilters');
checked = data.ignoreGenericCosmeticFilters === true || append && elem.checked;
elem.checked = listsetDetails.ignoreGenericCosmeticFilters = checked;
const selectedSet = new Set(data.selectedLists);
for ( const listEntry of qsa$('#lists .listEntry[data-role="leaf"]') ) {
const listkey = listEntry.dataset.key;
const mustEnable = selectedSet.has(listkey);
selectedSet.delete(listkey);
if ( mustEnable === false && append ) { continue; }
toggleFilterList(listEntry, mustEnable);
}
// If there are URL-like list keys left in the selected set, import them.
for ( const listkey of selectedSet ) {
if ( reValidExternalList.test(listkey) ) { continue; }
selectedSet.delete(listkey);
}
if ( selectedSet.size !== 0 ) {
const textarea = qs$('#lists .listEntry[data-role="import"] textarea');
const lines = append
? textarea.value.split(/[\n\r]+/)
: [];
lines.push(...selectedSet);
if ( lines.length !== 0 ) { lines.push(''); }
textarea.value = lines.join('\n');
dom.cl.toggle('#lists .listEntry[data-role="import"]', 'expanded', textarea.value !== '');
}
renderWidgets();
};
/******************************************************************************/
self.wikilink = 'https://github.com/gorhill/uBlock/wiki/Dashboard:-Filter-lists';
self.hasUnsavedData = function() {
return hashFromCurrentFromSettings() !== filteringSettingsHash;
};
/******************************************************************************/
renderFilterLists().then(( ) => {
const buttonUpdate = qs$('#buttonUpdate');
if ( dom.cl.has(buttonUpdate, 'active') ) { return; }
if ( dom.cl.has(buttonUpdate, 'disabled') ) { return; }
if ( listsetDetails.autoUpdate !== true ) { return; }
buttonUpdateHandler();
});
/******************************************************************************/

View File

@@ -0,0 +1,34 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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';
import { dom } from './dom.js';
/******************************************************************************/
(async ( ) => {
const appData = await vAPI.messaging.send('dashboard', {
what: 'getAppData',
});
dom.text('#aboutNameVer', appData.name + ' ' + appData.version);
})();

View File

@@ -0,0 +1,194 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2016-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
*/
/* global CodeMirror, uBlockDashboard */
'use strict';
import { dom, qs$ } from './dom.js';
/******************************************************************************/
let defaultSettings = new Map();
let adminSettings = new Map();
let beforeHash = '';
/******************************************************************************/
CodeMirror.defineMode('raw-settings', function() {
let lastSetting = '';
return {
token: function(stream) {
if ( stream.sol() ) {
stream.eatSpace();
const match = stream.match(/\S+/);
if ( match !== null && defaultSettings.has(match[0]) ) {
lastSetting = match[0];
return adminSettings.has(match[0])
? 'readonly keyword'
: 'keyword';
}
stream.skipToEnd();
return 'line-cm-error';
}
stream.eatSpace();
const match = stream.match(/.*$/);
if ( match !== null ) {
if ( match[0].trim() !== defaultSettings.get(lastSetting) ) {
return 'line-cm-strong';
}
if ( adminSettings.has(lastSetting) ) {
return 'readonly';
}
}
stream.skipToEnd();
return null;
}
};
});
const cmEditor = new CodeMirror(qs$('#advancedSettings'), {
autofocus: true,
lineNumbers: true,
lineWrapping: false,
styleActiveLine: true
});
uBlockDashboard.patchCodeMirrorEditor(cmEditor);
/******************************************************************************/
const hashFromAdvancedSettings = function(raw) {
const aa = typeof raw === 'string'
? arrayFromString(raw)
: arrayFromObject(raw);
aa.sort((a, b) => a[0].localeCompare(b[0]));
return JSON.stringify(aa);
};
/******************************************************************************/
const arrayFromObject = function(o) {
const out = [];
for ( const k in o ) {
if ( o.hasOwnProperty(k) === false ) { continue; }
out.push([ k, `${o[k]}` ]);
}
return out;
};
const arrayFromString = function(s) {
const out = [];
for ( let line of s.split(/[\n\r]+/) ) {
line = line.trim();
if ( line === '' ) { continue; }
const pos = line.indexOf(' ');
let k, v;
if ( pos !== -1 ) {
k = line.slice(0, pos);
v = line.slice(pos + 1);
} else {
k = line;
v = '';
}
out.push([ k.trim(), v.trim() ]);
}
return out;
};
/******************************************************************************/
const advancedSettingsChanged = (( ) => {
const handler = ( ) => {
const changed = hashFromAdvancedSettings(cmEditor.getValue()) !== beforeHash;
qs$('#advancedSettingsApply').disabled = !changed;
CodeMirror.commands.save = changed ? applyChanges : function(){};
};
const timer = vAPI.defer.create(handler);
return function() {
timer.offon(200);
};
})();
cmEditor.on('changes', advancedSettingsChanged);
/******************************************************************************/
const renderAdvancedSettings = async function(first) {
const details = await vAPI.messaging.send('dashboard', {
what: 'readHiddenSettings',
});
defaultSettings = new Map(arrayFromObject(details.default));
adminSettings = new Map(arrayFromObject(details.admin));
beforeHash = hashFromAdvancedSettings(details.current);
const pretty = [];
const roLines = [];
const entries = arrayFromObject(details.current);
let max = 0;
for ( const [ k ] of entries ) {
if ( k.length > max ) { max = k.length; }
}
for ( let i = 0; i < entries.length; i++ ) {
const [ k, v ] = entries[i];
pretty.push(' '.repeat(max - k.length) + `${k} ${v}`);
if ( adminSettings.has(k) ) {
roLines.push(i);
}
}
pretty.push('');
cmEditor.setValue(pretty.join('\n'));
if ( first ) {
cmEditor.clearHistory();
}
for ( const line of roLines ) {
cmEditor.markText(
{ line, ch: 0 },
{ line: line + 1, ch: 0 },
{ readOnly: true }
);
}
advancedSettingsChanged();
cmEditor.focus();
};
/******************************************************************************/
const applyChanges = async function() {
await vAPI.messaging.send('dashboard', {
what: 'writeHiddenSettings',
content: cmEditor.getValue(),
});
renderAdvancedSettings();
};
/******************************************************************************/
dom.on('#advancedSettings', 'input', advancedSettingsChanged);
dom.on('#advancedSettingsApply', 'click', ( ) => {
applyChanges();
});
renderAdvancedSettings(true);
/******************************************************************************/

View File

@@ -0,0 +1,116 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2020-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
*/
/******************************************************************************/
export class ArglistParser {
constructor(separatorChar = ',', mustQuote = false) {
this.separatorChar = this.actualSeparatorChar = separatorChar;
this.separatorCode = this.actualSeparatorCode = separatorChar.charCodeAt(0);
this.mustQuote = mustQuote;
this.quoteBeg = 0; this.quoteEnd = 0;
this.argBeg = 0; this.argEnd = 0;
this.separatorBeg = 0; this.separatorEnd = 0;
this.transform = false;
this.failed = false;
this.reWhitespaceStart = /^\s+/;
this.reWhitespaceEnd = /\s+$/;
this.reOddTrailingEscape = /(?:^|[^\\])(?:\\\\)*\\$/;
this.reTrailingEscapeChars = /\\+$/;
}
nextArg(pattern, beg = 0) {
const len = pattern.length;
this.quoteBeg = beg + this.leftWhitespaceCount(pattern.slice(beg));
this.failed = false;
const qc = pattern.charCodeAt(this.quoteBeg);
if ( qc === 0x22 /* " */ || qc === 0x27 /* ' */ || qc === 0x60 /* ` */ ) {
this.indexOfNextArgSeparator(pattern, qc);
if ( this.argEnd !== len ) {
this.quoteEnd = this.argEnd + 1;
this.separatorBeg = this.separatorEnd = this.quoteEnd;
this.separatorEnd += this.leftWhitespaceCount(pattern.slice(this.quoteEnd));
if ( this.separatorEnd === len ) { return this; }
if ( pattern.charCodeAt(this.separatorEnd) === this.separatorCode ) {
this.separatorEnd += 1;
return this;
}
}
}
this.indexOfNextArgSeparator(pattern, this.separatorCode);
this.separatorBeg = this.separatorEnd = this.argEnd;
if ( this.separatorBeg < len ) {
this.separatorEnd += 1;
}
this.argEnd -= this.rightWhitespaceCount(pattern.slice(0, this.separatorBeg));
this.quoteEnd = this.argEnd;
if ( this.mustQuote ) {
this.failed = true;
}
return this;
}
normalizeArg(s, char = '') {
if ( char === '' ) { char = this.actualSeparatorChar; }
let out = '';
let pos = 0;
while ( (pos = s.lastIndexOf(char)) !== -1 ) {
out = s.slice(pos) + out;
s = s.slice(0, pos);
const match = this.reTrailingEscapeChars.exec(s);
if ( match === null ) { continue; }
const tail = (match[0].length & 1) !== 0
? match[0].slice(0, -1)
: match[0];
out = tail + out;
s = s.slice(0, -match[0].length);
}
if ( out === '' ) { return s; }
return s + out;
}
leftWhitespaceCount(s) {
const match = this.reWhitespaceStart.exec(s);
return match === null ? 0 : match[0].length;
}
rightWhitespaceCount(s) {
const match = this.reWhitespaceEnd.exec(s);
return match === null ? 0 : match[0].length;
}
indexOfNextArgSeparator(pattern, separatorCode) {
this.argBeg = this.argEnd = separatorCode !== this.separatorCode
? this.quoteBeg + 1
: this.quoteBeg;
this.transform = false;
if ( separatorCode !== this.actualSeparatorCode ) {
this.actualSeparatorCode = separatorCode;
this.actualSeparatorChar = String.fromCharCode(separatorCode);
}
while ( this.argEnd < pattern.length ) {
const pos = pattern.indexOf(this.actualSeparatorChar, this.argEnd);
if ( pos === -1 ) {
return (this.argEnd = pattern.length);
}
if ( this.reOddTrailingEscape.test(pattern.slice(0, pos)) === false ) {
return (this.argEnd = pos);
}
this.transform = true;
this.argEnd = pos + 1;
}
}
}

View File

@@ -0,0 +1,113 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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
*/
/* global CodeMirror, uBlockDashboard */
'use strict';
/******************************************************************************/
import { dom, qs$ } from './dom.js';
import './codemirror/ubo-static-filtering.js';
/******************************************************************************/
(async ( ) => {
const subscribeURL = new URL(document.location);
const subscribeParams = subscribeURL.searchParams;
const assetKey = subscribeParams.get('url');
if ( assetKey === null ) { return; }
const subscribeElem = subscribeParams.get('subscribe') !== null
? qs$('#subscribe')
: null;
if ( subscribeElem !== null && subscribeURL.hash !== '#subscribed' ) {
const title = subscribeParams.get('title');
const promptElem = qs$('#subscribePrompt');
dom.text(promptElem.children[0], title);
const a = promptElem.children[1];
dom.text(a, assetKey);
dom.attr(a, 'href', assetKey);
dom.cl.remove(subscribeElem, 'hide');
}
const cmEditor = new CodeMirror(qs$('#content'), {
autofocus: true,
foldGutter: true,
gutters: [
'CodeMirror-linenumbers',
{ className: 'CodeMirror-lintgutter', style: 'width: 11px' },
],
lineNumbers: true,
lineWrapping: true,
matchBrackets: true,
maxScanLines: 1,
maximizable: false,
readOnly: true,
styleActiveLine: {
nonEmpty: true,
},
});
uBlockDashboard.patchCodeMirrorEditor(cmEditor);
vAPI.messaging.send('dashboard', {
what: 'getAutoCompleteDetails'
}).then(hints => {
if ( hints instanceof Object === false ) { return; }
cmEditor.setOption('uboHints', hints);
});
vAPI.messaging.send('dashboard', {
what: 'getTrustedScriptletTokens',
}).then(tokens => {
cmEditor.setOption('trustedScriptletTokens', tokens);
});
const details = await vAPI.messaging.send('default', {
what : 'getAssetContent',
url: assetKey,
});
cmEditor.setOption('trustedSource', details.trustedSource === true);
cmEditor.setValue(details && details.content || '');
if ( subscribeElem !== null ) {
dom.on('#subscribeButton', 'click', ( ) => {
dom.cl.add(subscribeElem, 'hide');
vAPI.messaging.send('scriptlets', {
what: 'applyFilterListSelection',
toImport: assetKey,
}).then(( ) => {
vAPI.messaging.send('scriptlets', {
what: 'reloadAllFilters'
});
});
}, { once: true });
}
if ( details.sourceURL ) {
const a = qs$('.cm-search-widget .sourceURL');
dom.attr(a, 'href', details.sourceURL);
dom.attr(a, 'title', details.sourceURL);
}
dom.cl.remove(dom.body, 'loading');
})();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,402 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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
*/
/******************************************************************************/
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.
if ( vAPI.webextFlavor === undefined ) {
vAPI.webextFlavor = { major: 0, soup: new Set([ 'ublock' ]) };
}
/******************************************************************************/
const hiddenSettingsDefault = {
allowGenericProceduralFilters: false,
assetFetchTimeout: 30,
autoCommentFilterTemplate: '{{date}} {{origin}}',
autoUpdateAssetFetchPeriod: 5,
autoUpdateDelayAfterLaunch: 37,
autoUpdatePeriod: 1,
benchmarkDatasetURL: 'unset',
blockingProfiles: '11111/#F00 11010/#C0F 11001/#00F 00001',
cacheStorageCompression: true,
cacheStorageCompressionThreshold: 65536,
cacheStorageMultithread: 2,
cacheControlForFirefox1376932: 'no-cache, no-store, must-revalidate',
cloudStorageCompression: true,
cnameIgnoreList: 'unset',
cnameIgnore1stParty: true,
cnameIgnoreExceptions: true,
cnameIgnoreRootDocument: true,
cnameReplayFullURL: false,
consoleLogLevel: 'unset',
debugAssetsJson: false,
debugScriptlets: false,
debugScriptletInjector: false,
differentialUpdate: true,
disableWebAssembly: false,
dnsCacheTTL: 600,
dnsResolveEnabled: true,
extensionUpdateForceReload: false,
filterAuthorMode: false,
loggerPopupType: 'popup',
manualUpdateAssetFetchPeriod: 500,
modifyWebextFlavor: 'unset',
noScriptingCSP: 'script-src http: https:',
popupFontSize: 'unset',
popupPanelDisabledSections: 0,
popupPanelHeightMode: 0,
popupPanelLockedSections: 0,
popupPanelOrientation: 'unset',
requestJournalProcessPeriod: 1000,
requestStatsDisabled: false,
selfieDelayInSeconds: 53,
strictBlockingBypassDuration: 120,
toolbarWarningTimeout: 60,
trustedListPrefixes: 'ublock-',
uiPopupConfig: 'unset',
uiStyles: 'unset',
updateAssetBypassBrowserCache: false,
userResourcesLocation: 'unset',
};
if ( vAPI.webextFlavor.soup.has('devbuild') ) {
hiddenSettingsDefault.consoleLogLevel = 'info';
hiddenSettingsDefault.cacheStorageAPI = 'unset';
ubologSet(true);
}
const userSettingsDefault = {
advancedUserEnabled: false,
alwaysDetachLogger: true,
autoUpdate: true,
cloudStorageEnabled: false,
cnameUncloakEnabled: true,
collapseBlocked: true,
colorBlindFriendly: false,
contextMenuEnabled: true,
uiAccentCustom: false,
uiAccentCustom0: '#aca0f7',
uiTheme: 'auto',
externalLists: '',
firewallPaneMinimized: true,
hyperlinkAuditingDisabled: true,
ignoreGenericCosmeticFilters: false,
importedLists: [],
largeMediaSize: 50,
parseAllABPHideFilters: true,
popupPanelSections: 0b111,
prefetchingDisabled: true,
requestLogMaxEntries: 1000,
showIconBadge: true,
suspendUntilListsAreLoaded: vAPI.Net.canSuspend(),
tooltipsDisabled: false,
userFiltersTrusted: false,
webrtcIPAddressHidden: false,
};
const dynamicFilteringDefault = [
'behind-the-scene * * noop',
'behind-the-scene * image noop',
'behind-the-scene * 3p noop',
'behind-the-scene * inline-script noop',
'behind-the-scene * 1p-script noop',
'behind-the-scene * 3p-script noop',
'behind-the-scene * 3p-frame noop',
];
const hostnameSwitchesDefault = [
'no-large-media: behind-the-scene false',
];
// https://github.com/LiCybora/NanoDefenderFirefox/issues/196
if ( vAPI.webextFlavor.soup.has('firefox') ) {
hostnameSwitchesDefault.push('no-csp-reports: * true');
}
const µBlock = { // jshint ignore:line
alarmQueue: [],
userSettingsDefault,
userSettings: Object.assign({}, userSettingsDefault),
hiddenSettingsDefault,
hiddenSettingsAdmin: {},
hiddenSettings: Object.assign({}, hiddenSettingsDefault),
dynamicFilteringDefault,
hostnameSwitchesDefault,
noDashboard: false,
// Features detection.
privacySettingsSupported: vAPI.browserSettings instanceof Object,
cloudStorageSupported: vAPI.cloud instanceof Object,
canFilterResponseData: typeof browser.webRequest.filterResponseData === 'function',
// https://github.com/chrisaljoudi/uBlock/issues/180
// Whitelist directives need to be loaded once the PSL is available
netWhitelist: new Map(),
netWhitelistModifyTime: 0,
netWhitelistDefault: [
'chrome-extension-scheme',
'moz-extension-scheme',
],
requestStats: {
blockedCount: 0,
allowedCount: 0,
},
// Read-only
systemSettings: {
compiledMagic: 57, // Increase when compiled format changes
selfieMagic: 58, // Increase when selfie format changes
},
// https://github.com/uBlockOrigin/uBlock-issues/issues/759#issuecomment-546654501
// The assumption is that cache storage state reflects whether
// compiled or selfie assets are available or not. The properties
// below is to no longer rely on this assumption -- though it's still
// not clear how the assumption could be wrong, and it's still not
// clear whether relying on those properties will really solve the
// issue. It's just an attempt at hardening.
compiledFormatChanged: false,
selfieIsInvalid: false,
restoreBackupSettings: {
lastRestoreFile: '',
lastRestoreTime: 0,
lastBackupFile: '',
lastBackupTime: 0,
},
commandShortcuts: new Map(),
// Allows to fully customize uBO's assets, typically set through admin
// settings. The content of 'assets.json' will also tell which filter
// lists to enable by default when uBO is first installed.
assetsBootstrapLocation: undefined,
assetsJsonPath: vAPI.webextFlavor.soup.has('devbuild')
? '/assets/assets.dev.json'
: '/assets/assets.json',
userFiltersPath: 'user-filters',
pslAssetKey: 'public_suffix_list.dat',
selectedFilterLists: [],
availableFilterLists: {},
badLists: new Map(),
inMemoryFilters: [],
inMemoryFiltersCompiled: '',
// https://github.com/uBlockOrigin/uBlock-issues/issues/974
// This can be used to defer filtering decision-making.
readyToFilter: false,
supportStats: {
allReadyAfter: '?',
maxAssetCacheWait: '?',
},
pageStores: new Map(),
pageStoresToken: 0,
storageQuota: vAPI.storage.QUOTA_BYTES,
storageUsed: 0,
noopFunc: function(){},
apiErrorCount: 0,
maybeGoodPopup: {
tabId: 0,
url: '',
},
epickerArgs: {
eprom: null,
mouse: false,
target: '',
zap: false,
},
scriptlets: {},
cspNoInlineScript: "script-src 'unsafe-eval' * blob: data:",
cspNoInlineFont: 'font-src *',
liveBlockingProfiles: [],
blockingProfileColorCache: new Map(),
parsedTrustedListPrefixes: [],
uiAccentStylesheet: '',
};
µBlock.isReadyPromise = new Promise(resolve => {
µBlock.isReadyResolve = resolve;
});
µBlock.domainFromHostname = domainFromHostname;
µBlock.hostnameFromURI = hostnameFromURI;
µBlock.FilteringContext = class extends FilteringContext {
duplicate() {
return (new µBlock.FilteringContext(this));
}
fromTabId(tabId) {
const tabContext = µBlock.tabContextManager.mustLookup(tabId);
this.tabOrigin = tabContext.origin;
this.tabHostname = tabContext.rootHostname;
this.tabDomain = tabContext.rootDomain;
this.tabId = tabContext.tabId;
return this;
}
maybeFromDocumentURL(documentUrl) {
if ( documentUrl === undefined ) { return; }
if ( documentUrl.startsWith(this.tabOrigin) ) { return; }
this.tabOrigin = originFromURI(µBlock.normalizeTabURL(0, documentUrl));
this.tabHostname = hostnameFromURI(this.tabOrigin);
this.tabDomain = domainFromHostname(this.tabHostname);
}
// https://github.com/uBlockOrigin/uBlock-issues/issues/459
// In case of a request for frame and if ever no context is specified,
// assume the origin of the context is the same as the request itself.
fromWebrequestDetails(details) {
const tabId = details.tabId;
this.type = details.type;
const isMainFrame = this.itype === this.MAIN_FRAME;
if ( isMainFrame && tabId > 0 ) {
µBlock.tabContextManager.push(tabId, details.url);
}
this.fromTabId(tabId); // Must be called AFTER tab context management
this.realm = '';
this.setMethod(details.method);
this.setURL(details.url);
this.setIPAddress(details.ip);
this.aliasURL = details.aliasURL || undefined;
this.redirectURL = undefined;
this.filter = undefined;
if ( this.itype !== this.SUB_FRAME ) {
this.docId = details.frameId;
this.frameId = -1;
} else {
this.docId = details.parentFrameId;
this.frameId = details.frameId;
}
if ( this.tabId > 0 ) {
if ( this.docId === 0 ) {
if ( isMainFrame === false ) {
this.maybeFromDocumentURL(details.documentUrl);
}
this.docOrigin = this.tabOrigin;
this.docHostname = this.tabHostname;
this.docDomain = this.tabDomain;
return this;
}
if ( details.documentUrl !== undefined ) {
this.setDocOriginFromURL(details.documentUrl);
return this;
}
const pageStore = µBlock.pageStoreFromTabId(this.tabId);
const docStore = pageStore && pageStore.getFrameStore(this.docId);
if ( docStore ) {
this.setDocOriginFromURL(docStore.rawURL);
} else {
this.setDocOrigin(this.tabOrigin);
}
return this;
}
if ( details.documentUrl !== undefined ) {
const origin = originFromURI(
µBlock.normalizeTabURL(0, details.documentUrl)
);
this.setDocOrigin(origin).setTabOrigin(origin);
return this;
}
const origin = this.isDocument()
? originFromURI(this.url)
: this.tabOrigin;
this.setDocOrigin(origin).setTabOrigin(origin);
return this;
}
getTabOrigin() {
if ( this.tabOrigin === undefined ) {
const tabContext = µBlock.tabContextManager.mustLookup(this.tabId);
this.tabOrigin = tabContext.origin;
this.tabHostname = tabContext.rootHostname;
this.tabDomain = tabContext.rootDomain;
}
return super.getTabOrigin();
}
toLogger() {
const details = {
tstamp: 0,
realm: this.realm,
method: this.getMethodName(),
type: this.stype,
tabId: this.tabId,
tabDomain: this.getTabDomain(),
tabHostname: this.getTabHostname(),
docDomain: this.getDocDomain(),
docHostname: this.getDocHostname(),
domain: this.getDomain(),
hostname: this.getHostname(),
url: this.url,
aliasURL: this.aliasURL,
filter: undefined,
};
// Many filters may have been applied to the current context
if ( Array.isArray(this.filter) === false ) {
details.filter = this.filter;
return logger.writeOne(details);
}
for ( const filter of this.filter ) {
details.filter = filter;
logger.writeOne(details);
}
}
};
µBlock.filteringContext = new µBlock.FilteringContext();
self.µBlock = µBlock;
/******************************************************************************/
export default µBlock;

View File

@@ -0,0 +1,145 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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';
/******************************************************************************/
// Custom base64 codecs. These codecs are meant to encode/decode typed arrays
// to/from strings.
// https://github.com/uBlockOrigin/uBlock-issues/issues/461
// Provide a fallback encoding for Chromium 59 and less by issuing a plain
// JSON string. The fallback can be removed once min supported version is
// above 59.
// TODO: rename µBlock.base64 to µBlock.SparseBase64, now that
// µBlock.DenseBase64 has been introduced.
// TODO: Should no longer need to test presence of TextEncoder/TextDecoder.
const valToDigit = new Uint8Array(64);
const digitToVal = new Uint8Array(128);
{
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz@%';
for ( let i = 0, n = chars.length; i < n; i++ ) {
const c = chars.charCodeAt(i);
valToDigit[i] = c;
digitToVal[c] = i;
}
}
// 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
// random-looking.
// TODO: Investigate that in Firefox, creating a new Uint8Array from the
// ArrayBuffer fails, the content of the resulting Uint8Array is
// non-sensical. WASM-related?
export const denseBase64 = {
magic: 'DenseBase64_1',
encode: function(input) {
const m = input.length % 3;
const n = input.length - m;
let outputLength = n / 3 * 4;
if ( m !== 0 ) {
outputLength += m + 1;
}
const output = new Uint8Array(outputLength);
let j = 0;
for ( let i = 0; i < n; i += 3) {
const i1 = input[i+0];
const i2 = input[i+1];
const i3 = input[i+2];
output[j+0] = valToDigit[ i1 >>> 2];
output[j+1] = valToDigit[i1 << 4 & 0b110000 | i2 >>> 4];
output[j+2] = valToDigit[i2 << 2 & 0b111100 | i3 >>> 6];
output[j+3] = valToDigit[i3 & 0b111111 ];
j += 4;
}
if ( m !== 0 ) {
const i1 = input[n];
output[j+0] = valToDigit[i1 >>> 2];
if ( m === 1 ) { // 1 value
output[j+1] = valToDigit[i1 << 4 & 0b110000];
} else { // 2 values
const i2 = input[n+1];
output[j+1] = valToDigit[i1 << 4 & 0b110000 | i2 >>> 4];
output[j+2] = valToDigit[i2 << 2 & 0b111100 ];
}
}
const textDecoder = new TextDecoder();
const b64str = textDecoder.decode(output);
return this.magic + b64str;
},
decode: function(instr, arrbuf) {
if ( instr.startsWith(this.magic) === false ) {
throw new Error('Invalid µBlock.denseBase64 encoding');
}
const outputLength = this.decodeSize(instr);
const outbuf = arrbuf instanceof ArrayBuffer === false
? new Uint8Array(outputLength)
: new Uint8Array(arrbuf);
const inputLength = instr.length - this.magic.length;
let i = this.magic.length;
let j = 0;
const m = inputLength & 3;
const n = i + inputLength - m;
while ( i < n ) {
const i1 = digitToVal[instr.charCodeAt(i+0)];
const i2 = digitToVal[instr.charCodeAt(i+1)];
const i3 = digitToVal[instr.charCodeAt(i+2)];
const i4 = digitToVal[instr.charCodeAt(i+3)];
i += 4;
outbuf[j+0] = i1 << 2 | i2 >>> 4;
outbuf[j+1] = i2 << 4 & 0b11110000 | i3 >>> 2;
outbuf[j+2] = i3 << 6 & 0b11000000 | i4;
j += 3;
}
if ( m !== 0 ) {
const i1 = digitToVal[instr.charCodeAt(i+0)];
const i2 = digitToVal[instr.charCodeAt(i+1)];
outbuf[j+0] = i1 << 2 | i2 >>> 4;
if ( m === 3 ) {
const i3 = digitToVal[instr.charCodeAt(i+2)];
outbuf[j+1] = i2 << 4 & 0b11110000 | i3 >>> 2;
}
}
return outbuf;
},
decodeSize: function(instr) {
if ( instr.startsWith(this.magic) === false ) { return 0; }
const inputLength = instr.length - this.magic.length;
const m = inputLength & 3;
const n = inputLength - m;
let outputLength = (n >>> 2) * 3;
if ( m !== 0 ) {
outputLength += m - 1;
}
return outputLength;
},
};
/******************************************************************************/

View File

@@ -0,0 +1,441 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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
*/
import {
domainFromHostname,
entityFromDomain,
hostnameFromURI,
} from './uri-utils.js';
import { FilteringContext } from './filtering-context.js';
import { LineIterator } from './text-utils.js';
import cosmeticFilteringEngine from './cosmetic-filtering.js';
import io from './assets.js';
import scriptletFilteringEngine from './scriptlet-filtering.js';
import { sessionFirewall } from './filtering-engines.js';
import { default as sfne } from './static-net-filtering.js';
import webRequest from './traffic.js';
import µb from './background.js';
/******************************************************************************/
// The requests.json.gz file can be downloaded from:
// https://cdn.cliqz.com/adblocking/requests_top500.json.gz
//
// Which is linked from:
// https://whotracks.me/blog/adblockers_performance_study.html
//
// Copy the file into ./tmp/requests.json.gz
//
// If the file is present when you build uBO using `make-[target].sh` from
// the shell, the resulting package will have `./assets/requests.json`, which
// will be looked-up by the method below to launch a benchmark session.
//
// From uBO's dev console, launch the benchmark:
// µBlock.staticNetFilteringEngine.benchmark();
//
// The usual browser dev tools can be used to obtain useful profiling
// data, i.e. start the profiler, call the benchmark method from the
// console, then stop the profiler when it completes.
//
// Keep in mind that the measurements at the blog post above where obtained
// with ONLY EasyList. The CPU reportedly used was:
// https://www.cpubenchmark.net/cpu.php?cpu=Intel+Core+i7-6600U+%40+2.60GHz&id=2608
//
// Rename ./tmp/requests.json.gz to something else if you no longer want
// ./assets/requests.json in the build.
const loadBenchmarkDataset = (( ) => {
let datasetPromise;
const ttlTimer = vAPI.defer.create(( ) => {
datasetPromise = undefined;
});
return async function() {
ttlTimer.offon({ min: 2 });
if ( datasetPromise !== undefined ) {
return datasetPromise;
}
const datasetURL = µb.hiddenSettings.benchmarkDatasetURL;
if ( datasetURL === 'unset' ) {
console.info(`No benchmark dataset available.`);
return;
}
console.info(`Loading benchmark dataset...`);
datasetPromise = io.fetchText(datasetURL).then(details => {
console.info(`Parsing benchmark dataset...`);
let requests = [];
if ( details.content.startsWith('[') ) {
try {
requests = JSON.parse(details.content);
} catch(ex) {
}
} else {
const lineIter = new LineIterator(details.content);
const parsed = [];
while ( lineIter.eot() === false ) {
const line = lineIter.next().trim();
if ( line === '' ) { continue; }
try {
parsed.push(JSON.parse(line));
} catch(ex) {
parsed.length = 0;
break;
}
}
requests = parsed;
}
if ( requests.length === 0 ) { return; }
const out = [];
for ( const request of requests ) {
if ( request instanceof Object === false ) { continue; }
if ( !request.frameUrl || !request.url ) { continue; }
if ( request.cpt === 'document' ) {
request.cpt = 'main_frame';
} else if ( request.cpt === 'xhr' ) {
request.cpt = 'xmlhttprequest';
}
out.push(request);
}
return out;
}).catch(details => {
console.info(`Not found: ${details.url}`);
datasetPromise = undefined;
});
return datasetPromise;
};
})();
/******************************************************************************/
export async function benchmarkStaticNetFiltering(options = {}) {
const { target, redirectEngine } = options;
const requests = await loadBenchmarkDataset();
if ( Array.isArray(requests) === false || requests.length === 0 ) {
const text = 'No dataset found to benchmark';
console.info(text);
return text;
}
console.info(`Benchmarking staticNetFilteringEngine.matchRequest()...`);
const fctxt = new FilteringContext();
if ( typeof target === 'number' ) {
const request = requests[target];
fctxt.setURL(request.url);
fctxt.setDocOriginFromURL(request.frameUrl);
fctxt.setType(request.cpt);
const r = sfne.matchRequest(fctxt);
console.info(`Result=${r}:`);
console.info(`\ttype=${fctxt.type}`);
console.info(`\turl=${fctxt.url}`);
console.info(`\tdocOrigin=${fctxt.getDocOrigin()}`);
if ( r !== 0 ) {
console.info(sfne.toLogData());
}
return;
}
const t0 = performance.now();
let matchCount = 0;
let blockCount = 0;
let allowCount = 0;
let redirectCount = 0;
let removeparamCount = 0;
let urlskipCount = 0;
let cspCount = 0;
let permissionsCount = 0;
let replaceCount = 0;
for ( const request of requests ) {
fctxt.setURL(request.url);
if ( fctxt.getIPAddress() === '' ) {
fctxt.setIPAddress('93.184.215.14\n2606:2800:21f:cb07:6820:80da:af6b:8b2c');
}
fctxt.setDocOriginFromURL(request.frameUrl);
fctxt.setType(request.cpt);
sfne.redirectURL = undefined;
const r = sfne.matchRequest(fctxt);
matchCount += 1;
if ( r === 1 ) { blockCount += 1; }
else if ( r === 2 ) { allowCount += 1; }
if ( r !== 1 ) {
if ( sfne.transformRequest(fctxt) ) {
redirectCount += 1;
}
if ( sfne.hasQuery(fctxt) ) {
if ( sfne.filterQuery(fctxt) ) {
removeparamCount += 1;
}
}
if ( sfne.urlSkip(fctxt, false) ) {
urlskipCount += 1;
}
if ( fctxt.isDocument() ) {
if ( sfne.matchAndFetchModifiers(fctxt, 'csp') ) {
cspCount += 1;
}
if ( sfne.matchAndFetchModifiers(fctxt, 'permissions') ) {
permissionsCount += 1;
}
}
sfne.matchHeaders(fctxt, []);
if ( sfne.matchAndFetchModifiers(fctxt, 'replace') ) {
replaceCount += 1;
}
} else if ( redirectEngine !== undefined ) {
if ( sfne.redirectRequest(redirectEngine, fctxt) ) {
redirectCount += 1;
}
if ( fctxt.isRootDocument() && sfne.urlSkip(fctxt, true) ) {
urlskipCount += 1;
}
}
}
const t1 = performance.now();
const dur = t1 - t0;
const output = [
'Benchmarked static network filtering engine:',
`\tEvaluated ${matchCount} requests in ${dur.toFixed(0)} ms`,
`\tAverage: ${(dur / matchCount).toFixed(3)} ms per request`,
`\tNot blocked: ${matchCount - blockCount - allowCount}`,
`\tBlocked: ${blockCount}`,
`\tUnblocked: ${allowCount}`,
`\tredirect=: ${redirectCount}`,
`\tremoveparam=: ${removeparamCount}`,
`\turlskip=: ${urlskipCount}`,
`\tcsp=: ${cspCount}`,
`\tpermissions=: ${permissionsCount}`,
`\treplace=: ${replaceCount}`,
];
const s = output.join('\n');
console.info(s);
return s;
}
/******************************************************************************/
export async function tokenHistogramsfunction() {
const requests = await loadBenchmarkDataset();
if ( Array.isArray(requests) === false || requests.length === 0 ) {
console.info('No requests found to benchmark');
return;
}
console.info(`Computing token histograms...`);
const fctxt = new FilteringContext();
const missTokenMap = new Map();
const hitTokenMap = new Map();
const reTokens = /[0-9a-z%]{2,}/g;
for ( let i = 0; i < requests.length; i++ ) {
const request = requests[i];
fctxt.setURL(request.url);
fctxt.setDocOriginFromURL(request.frameUrl);
fctxt.setType(request.cpt);
const r = sfne.matchRequest(fctxt);
for ( let [ keyword ] of request.url.toLowerCase().matchAll(reTokens) ) {
const token = keyword.slice(0, 7);
if ( r === 0 ) {
missTokenMap.set(token, (missTokenMap.get(token) || 0) + 1);
} else if ( r === 1 ) {
hitTokenMap.set(token, (hitTokenMap.get(token) || 0) + 1);
}
}
}
const customSort = (a, b) => b[1] - a[1];
const topmisses = Array.from(missTokenMap).sort(customSort).slice(0, 100);
for ( const [ token ] of topmisses ) {
hitTokenMap.delete(token);
}
const tophits = Array.from(hitTokenMap).sort(customSort).slice(0, 100);
console.info('Misses:', JSON.stringify(topmisses));
console.info('Hits:', JSON.stringify(tophits));
}
/******************************************************************************/
export async function benchmarkDynamicNetFiltering() {
const requests = await loadBenchmarkDataset();
if ( Array.isArray(requests) === false || requests.length === 0 ) {
console.info('No requests found to benchmark');
return;
}
console.info(`Benchmarking sessionFirewall.evaluateCellZY()...`);
const fctxt = new FilteringContext();
const t0 = performance.now();
for ( const request of requests ) {
fctxt.setURL(request.url);
fctxt.setTabOriginFromURL(request.frameUrl);
fctxt.setType(request.cpt);
sessionFirewall.evaluateCellZY(
fctxt.getTabHostname(),
fctxt.getHostname(),
fctxt.type
);
}
const t1 = performance.now();
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`);
}
/******************************************************************************/
export async function benchmarkCosmeticFiltering() {
const requests = await loadBenchmarkDataset();
if ( Array.isArray(requests) === false || requests.length === 0 ) {
console.info('No requests found to benchmark');
return;
}
const output = [
'Benchmarking cosmeticFilteringEngine.retrieveSpecificSelectors()...',
];
const details = {
tabId: undefined,
frameId: undefined,
hostname: '',
domain: '',
entity: '',
};
const options = {
noSpecificCosmeticFiltering: false,
noGenericCosmeticFiltering: false,
dontInject: true,
};
let count = 0;
const t0 = performance.now();
for ( let i = 0; i < requests.length; i++ ) {
const request = requests[i];
if ( request.cpt !== 'main_frame' ) { continue; }
count += 1;
details.hostname = hostnameFromURI(request.url);
details.domain = domainFromHostname(details.hostname);
details.entity = entityFromDomain(details.domain);
void cosmeticFilteringEngine.retrieveSpecificSelectors(details, options);
}
const t1 = performance.now();
const dur = t1 - t0;
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;
}
/******************************************************************************/
export async function benchmarkScriptletFiltering() {
const requests = await loadBenchmarkDataset();
if ( Array.isArray(requests) === false || requests.length === 0 ) {
console.info('No requests found to benchmark');
return;
}
const output = [
'Benchmarking scriptletFilteringEngine.retrieve()...',
];
const details = {
domain: '',
entity: '',
hostname: '',
tabId: 0,
url: '',
nocache: true,
};
let count = 0;
const t0 = performance.now();
for ( let i = 0; i < requests.length; i++ ) {
const request = requests[i];
if ( request.cpt !== 'main_frame' ) { continue; }
count += 1;
details.url = request.url;
details.hostname = hostnameFromURI(request.url);
details.domain = domainFromHostname(details.hostname);
details.entity = entityFromDomain(details.domain);
void scriptletFilteringEngine.retrieve(details);
}
const t1 = performance.now();
const dur = t1 - t0;
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;
}
/******************************************************************************/
export async function benchmarkOnBeforeRequest() {
const requests = await loadBenchmarkDataset();
if ( Array.isArray(requests) === false || requests.length === 0 ) {
console.info('No requests found to benchmark');
return;
}
const mappedTypes = new Map([
[ 'document', 'main_frame' ],
[ 'subdocument', 'sub_frame' ],
]);
console.info('webRequest.onBeforeRequest()...');
const t0 = self.performance.now();
const promises = [];
const details = {
documentUrl: '',
tabId: -1,
parentFrameId: -1,
frameId: 0,
type: '',
url: '',
};
for ( const request of requests ) {
details.documentUrl = request.frameUrl;
details.tabId = -1;
details.parentFrameId = -1;
details.frameId = 0;
details.type = mappedTypes.get(request.cpt) || request.cpt;
details.url = request.url;
if ( details.type === 'main_frame' ) { continue; }
promises.push(webRequest.onBeforeRequest(details));
}
return Promise.all(promises).then(results => {
let blockCount = 0;
for ( const r of results ) {
if ( r !== undefined ) { blockCount += 1; }
}
const t1 = self.performance.now();
const dur = t1 - t0;
console.info(`Evaluated ${requests.length} requests in ${dur.toFixed(0)} ms`);
console.info(`\tBlocked ${blockCount} requests`);
console.info(`\tAverage: ${(dur / requests.length).toFixed(3)} ms per request`);
});
}
/******************************************************************************/

View File

@@ -0,0 +1,937 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2019-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
*/
/*******************************************************************************
A BidiTrieContainer is mostly a large buffer in which distinct but related
tries are stored. The memory layout of the buffer is as follow:
0-2047: haystack section
2048-2051: number of significant characters in the haystack
2052-2055: offset to start of trie data section (=> trie0)
2056-2059: offset to end of trie data section (=> trie1)
2060-2063: offset to start of character data section (=> char0)
2064-2067: offset to end of character data section (=> char1)
2068: start of trie data section
+--------------+
Normal cell: | And | If "Segment info" matches:
(aka CELL) +--------------+ Goto "And"
| Or | Else
+--------------+ Goto "Or"
| Segment info |
+--------------+
+--------------+
Boundary cell: | Right And | "Right And" and/or "Left And"
(aka BCELL) +--------------+ can be 0 in last-segment condition.
| Left And |
+--------------+
| 0 |
+--------------+
Given following filters and assuming token is "ad" for all of them:
-images/ad-
/google_ad.
/images_ad.
_images/ad.
We get the following internal representation:
+-----------+ +-----------+ +---+
| |---->| |---->| 0 |
+-----------+ +-----------+ +---+ +-----------+
| 0 | +--| | | |---->| 0 |
+-----------+ | +-----------+ +---+ +-----------+
| ad | | | - | | 0 | | 0 |
+-----------+ | +-----------+ +---+ +-----------+
| | -images/ |
| +-----------+ +---+ +-----------+
+->| |---->| 0 |
+-----------+ +---+ +-----------+ +-----------+
| 0 | | |---->| |---->| 0 |
+-----------+ +---+ +-----------+ +-----------+
| . | | 0 | +--| | +--| |
+-----------+ +---+ | +-----------+ | +-----------+
| | _ | | | /google |
| +-----------+ | +-----------+
| |
| | +-----------+
| +->| 0 |
| +-----------+
| | 0 |
| +-----------+
| | /images |
| +-----------+
|
| +-----------+
+->| 0 |
+-----------+
| 0 |
+-----------+
| _images/ |
+-----------+
*/
const PAGE_SIZE = 65536*2;
const HAYSTACK_START = 0;
const HAYSTACK_SIZE = 2048; // i32 / i8
const HAYSTACK_SIZE_SLOT = HAYSTACK_SIZE >>> 2; // 512 / 2048
const TRIE0_SLOT = HAYSTACK_SIZE_SLOT + 1; // 513 / 2052
const TRIE1_SLOT = HAYSTACK_SIZE_SLOT + 2; // 514 / 2056
const CHAR0_SLOT = HAYSTACK_SIZE_SLOT + 3; // 515 / 2060
const CHAR1_SLOT = HAYSTACK_SIZE_SLOT + 4; // 516 / 2064
const RESULT_L_SLOT = HAYSTACK_SIZE_SLOT + 5; // 517 / 2068
const RESULT_R_SLOT = HAYSTACK_SIZE_SLOT + 6; // 518 / 2072
const RESULT_IU_SLOT = HAYSTACK_SIZE_SLOT + 7; // 519 / 2076
const TRIE0_START = HAYSTACK_SIZE_SLOT + 8 << 2; // 2080
const CELL_BYTE_LENGTH = 12;
const MIN_FREE_CELL_BYTE_LENGTH = CELL_BYTE_LENGTH * 8;
const CELL_AND = 0;
const CELL_OR = 1;
const SEGMENT_INFO = 2;
const BCELL_NEXT_AND = 0;
const BCELL_ALT_AND = 1;
const BCELL_EXTRA = 2;
const BCELL_EXTRA_MAX = 0x00FFFFFF;
const toSegmentInfo = (aL, l, r) => ((r - l) << 24) | (aL + l);
const roundToPageSize = v => (v + PAGE_SIZE-1) & ~(PAGE_SIZE-1);
// http://www.cse.yorku.ca/~oz/hash.html#djb2
const i32Checksum = (buf32) => {
const n = buf32.length;
let hash = 177573 ^ n;
for ( let i = 0; i < n; i++ ) {
hash = (hash << 5) + hash ^ buf32[i];
}
return hash;
};
class BidiTrieContainer {
constructor(extraHandler) {
const len = PAGE_SIZE * 4;
this.buf8 = new Uint8Array(len);
this.buf32 = new Uint32Array(this.buf8.buffer);
this.buf32[TRIE0_SLOT] = TRIE0_START;
this.buf32[TRIE1_SLOT] = this.buf32[TRIE0_SLOT];
this.buf32[CHAR0_SLOT] = len >>> 1;
this.buf32[CHAR1_SLOT] = this.buf32[CHAR0_SLOT];
this.haystack = this.buf8.subarray(
HAYSTACK_START,
HAYSTACK_START + HAYSTACK_SIZE
);
this.extraHandler = extraHandler;
this.textDecoder = null;
this.wasmMemory = null;
this.lastStored = '';
this.lastStoredLen = this.lastStoredIndex = 0;
}
//--------------------------------------------------------------------------
// Public methods
//--------------------------------------------------------------------------
get haystackLen() {
return this.buf32[HAYSTACK_SIZE_SLOT];
}
set haystackLen(v) {
this.buf32[HAYSTACK_SIZE_SLOT] = v;
}
reset(details) {
if (
details instanceof Object &&
typeof details.byteLength === 'number' &&
typeof details.char0 === 'number'
) {
if ( details.byteLength > this.buf8.byteLength ) {
this.reallocateBuf(details.byteLength);
}
this.buf32[CHAR0_SLOT] = details.char0;
}
this.buf32[TRIE1_SLOT] = this.buf32[TRIE0_SLOT];
this.buf32[CHAR1_SLOT] = this.buf32[CHAR0_SLOT];
this.lastStored = '';
this.lastStoredLen = this.lastStoredIndex = 0;
}
createTrie() {
// grow buffer if needed
if ( (this.buf32[CHAR0_SLOT] - this.buf32[TRIE1_SLOT]) < CELL_BYTE_LENGTH ) {
this.growBuf(CELL_BYTE_LENGTH, 0);
}
const iroot = this.buf32[TRIE1_SLOT] >>> 2;
this.buf32[TRIE1_SLOT] += CELL_BYTE_LENGTH;
this.buf32[iroot+CELL_OR] = 0;
this.buf32[iroot+CELL_AND] = 0;
this.buf32[iroot+SEGMENT_INFO] = 0;
return iroot;
}
matches(icell, ai) {
const buf32 = this.buf32;
const buf8 = this.buf8;
const char0 = buf32[CHAR0_SLOT];
const aR = buf32[HAYSTACK_SIZE_SLOT];
let al = ai, x = 0, y = 0;
for (;;) {
x = buf8[al];
al += 1;
// find matching segment
for (;;) {
y = buf32[icell+SEGMENT_INFO];
let bl = char0 + (y & 0x00FFFFFF);
if ( buf8[bl] === x ) {
y = (y >>> 24) - 1;
if ( y !== 0 ) {
x = al + y;
if ( x > aR ) { return 0; }
for (;;) {
bl += 1;
if ( buf8[bl] !== buf8[al] ) { return 0; }
al += 1;
if ( al === x ) { break; }
}
}
break;
}
icell = buf32[icell+CELL_OR];
if ( icell === 0 ) { return 0; }
}
// next segment
icell = buf32[icell+CELL_AND];
x = buf32[icell+BCELL_EXTRA];
if ( x <= BCELL_EXTRA_MAX ) {
if ( x !== 0 && this.matchesExtra(ai, al, x) !== 0 ) {
return 1;
}
x = buf32[icell+BCELL_ALT_AND];
if ( x !== 0 && this.matchesLeft(x, ai, al) !== 0 ) {
return 1;
}
icell = buf32[icell+BCELL_NEXT_AND];
if ( icell === 0 ) { return 0; }
}
if ( al === aR ) { return 0; }
}
return 0; // eslint-disable-line no-unreachable
}
matchesLeft(icell, ar, r) {
const buf32 = this.buf32;
const buf8 = this.buf8;
const char0 = buf32[CHAR0_SLOT];
let x = 0, y = 0;
for (;;) {
if ( ar === 0 ) { return 0; }
ar -= 1;
x = buf8[ar];
// find first segment with a first-character match
for (;;) {
y = buf32[icell+SEGMENT_INFO];
let br = char0 + (y & 0x00FFFFFF);
y = (y >>> 24) - 1;
br += y;
if ( buf8[br] === x ) { // all characters in segment must match
if ( y !== 0 ) {
x = ar - y;
if ( x < 0 ) { return 0; }
for (;;) {
ar -= 1; br -= 1;
if ( buf8[ar] !== buf8[br] ) { return 0; }
if ( ar === x ) { break; }
}
}
break;
}
icell = buf32[icell+CELL_OR];
if ( icell === 0 ) { return 0; }
}
// next segment
icell = buf32[icell+CELL_AND];
x = buf32[icell+BCELL_EXTRA];
if ( x <= BCELL_EXTRA_MAX ) {
if ( x !== 0 && this.matchesExtra(ar, r, x) !== 0 ) {
return 1;
}
icell = buf32[icell+BCELL_NEXT_AND];
if ( icell === 0 ) { return 0; }
}
}
return 0; // eslint-disable-line no-unreachable
}
matchesExtra(l, r, ix) {
let iu = 0;
if ( ix !== 1 ) {
iu = this.extraHandler(l, r, ix);
if ( iu === 0 ) { return 0; }
} else {
iu = -1;
}
this.buf32[RESULT_IU_SLOT] = iu;
this.buf32[RESULT_L_SLOT] = l;
this.buf32[RESULT_R_SLOT] = r;
return 1;
}
get $l() { return this.buf32[RESULT_L_SLOT] | 0; }
get $r() { return this.buf32[RESULT_R_SLOT] | 0; }
get $iu() { return this.buf32[RESULT_IU_SLOT] | 0; }
add(iroot, aL0, n, pivot = 0) {
const aR = n;
if ( aR === 0 ) { return 0; }
// Grow buffer if needed. The characters are already in our character
// data buffer, so we do not need to grow character data buffer.
if (
(this.buf32[CHAR0_SLOT] - this.buf32[TRIE1_SLOT]) <
MIN_FREE_CELL_BYTE_LENGTH
) {
this.growBuf(MIN_FREE_CELL_BYTE_LENGTH, 0);
}
const buf32 = this.buf32;
const char0 = buf32[CHAR0_SLOT];
let icell = iroot;
let aL = char0 + aL0;
// special case: first node in trie
if ( buf32[icell+SEGMENT_INFO] === 0 ) {
buf32[icell+SEGMENT_INFO] = toSegmentInfo(aL0, pivot, aR);
return this.addLeft(icell, aL0, pivot);
}
const buf8 = this.buf8;
let al = pivot;
let inext;
// find a matching cell: move down
for (;;) {
const binfo = buf32[icell+SEGMENT_INFO];
// length of segment
const bR = binfo >>> 24;
// skip boundary cells
if ( bR === 0 ) {
icell = buf32[icell+BCELL_NEXT_AND];
continue;
}
let bl = char0 + (binfo & 0x00FFFFFF);
// if first character is no match, move to next descendant
if ( buf8[bl] !== buf8[aL+al] ) {
inext = buf32[icell+CELL_OR];
if ( inext === 0 ) {
inext = this.addCell(0, 0, toSegmentInfo(aL0, al, aR));
buf32[icell+CELL_OR] = inext;
return this.addLeft(inext, aL0, pivot);
}
icell = inext;
continue;
}
// 1st character was tested
let bi = 1;
al += 1;
// find 1st mismatch in rest of segment
if ( bR !== 1 ) {
for (;;) {
if ( bi === bR ) { break; }
if ( al === aR ) { break; }
if ( buf8[bl+bi] !== buf8[aL+al] ) { break; }
bi += 1;
al += 1;
}
}
// all segment characters matched
if ( bi === bR ) {
// needle remainder: no
if ( al === aR ) {
return this.addLeft(icell, aL0, pivot);
}
// needle remainder: yes
inext = buf32[icell+CELL_AND];
if ( buf32[inext+CELL_AND] !== 0 ) {
icell = inext;
continue;
}
// add needle remainder
icell = this.addCell(0, 0, toSegmentInfo(aL0, al, aR));
buf32[inext+CELL_AND] = icell;
return this.addLeft(icell, aL0, pivot);
}
// some characters matched
// split current segment
bl -= char0;
buf32[icell+SEGMENT_INFO] = bi << 24 | bl;
inext = this.addCell(
buf32[icell+CELL_AND], 0, bR - bi << 24 | bl + bi
);
buf32[icell+CELL_AND] = inext;
// needle remainder: no = need boundary cell
if ( al === aR ) {
return this.addLeft(icell, aL0, pivot);
}
// needle remainder: yes = need new cell for remaining characters
icell = this.addCell(0, 0, toSegmentInfo(aL0, al, aR));
buf32[inext+CELL_OR] = icell;
return this.addLeft(icell, aL0, pivot);
}
}
addLeft(icell, aL0, pivot) {
const buf32 = this.buf32;
const char0 = buf32[CHAR0_SLOT];
let aL = aL0 + char0;
// fetch boundary cell
let iboundary = buf32[icell+CELL_AND];
// add boundary cell if none exist
if (
iboundary === 0 ||
buf32[iboundary+SEGMENT_INFO] > BCELL_EXTRA_MAX
) {
const inext = iboundary;
iboundary = this.allocateCell();
buf32[icell+CELL_AND] = iboundary;
buf32[iboundary+BCELL_NEXT_AND] = inext;
if ( pivot === 0 ) { return iboundary; }
}
// shortest match with no extra conditions will always win
if ( buf32[iboundary+BCELL_EXTRA] === 1 ) {
return iboundary;
}
// bail out if no left segment
if ( pivot === 0 ) { return iboundary; }
// fetch root cell of left segment
icell = buf32[iboundary+BCELL_ALT_AND];
if ( icell === 0 ) {
icell = this.allocateCell();
buf32[iboundary+BCELL_ALT_AND] = icell;
}
// special case: first node in trie
if ( buf32[icell+SEGMENT_INFO] === 0 ) {
buf32[icell+SEGMENT_INFO] = toSegmentInfo(aL0, 0, pivot);
iboundary = this.allocateCell();
buf32[icell+CELL_AND] = iboundary;
return iboundary;
}
const buf8 = this.buf8;
let ar = pivot, inext;
// find a matching cell: move down
for (;;) {
const binfo = buf32[icell+SEGMENT_INFO];
// skip boundary cells
if ( binfo <= BCELL_EXTRA_MAX ) {
inext = buf32[icell+CELL_AND];
if ( inext !== 0 ) {
icell = inext;
continue;
}
iboundary = this.allocateCell();
buf32[icell+CELL_AND] =
this.addCell(iboundary, 0, toSegmentInfo(aL0, 0, ar));
// TODO: boundary cell might be last
// add remainder + boundary cell
return iboundary;
}
const bL = char0 + (binfo & 0x00FFFFFF);
const bR = bL + (binfo >>> 24);
let br = bR;
// if first character is no match, move to next descendant
if ( buf8[br-1] !== buf8[aL+ar-1] ) {
inext = buf32[icell+CELL_OR];
if ( inext === 0 ) {
iboundary = this.allocateCell();
inext = this.addCell(
iboundary, 0, toSegmentInfo(aL0, 0, ar)
);
buf32[icell+CELL_OR] = inext;
return iboundary;
}
icell = inext;
continue;
}
// 1st character was tested
br -= 1;
ar -= 1;
// find 1st mismatch in rest of segment
if ( br !== bL ) {
for (;;) {
if ( br === bL ) { break; }
if ( ar === 0 ) { break; }
if ( buf8[br-1] !== buf8[aL+ar-1] ) { break; }
br -= 1;
ar -= 1;
}
}
// all segment characters matched
// a: ...vvvvvvv
// b: vvvvvvv
if ( br === bL ) {
inext = buf32[icell+CELL_AND];
// needle remainder: no
// a: vvvvvvv
// b: vvvvvvv
// r: 0 & vvvvvvv
if ( ar === 0 ) {
// boundary cell already present
if ( buf32[inext+BCELL_EXTRA] <= BCELL_EXTRA_MAX ) {
return inext;
}
// need boundary cell
iboundary = this.allocateCell();
buf32[iboundary+CELL_AND] = inext;
buf32[icell+CELL_AND] = iboundary;
return iboundary;
}
// needle remainder: yes
// a: yyyyyyyvvvvvvv
// b: vvvvvvv
else {
if ( inext !== 0 ) {
icell = inext;
continue;
}
// TODO: we should never reach here because there will
// always be a boundary cell.
// eslint-disable-next-line no-debugger
debugger; // jshint ignore:line
// boundary cell + needle remainder
inext = this.addCell(0, 0, 0);
buf32[icell+CELL_AND] = inext;
buf32[inext+CELL_AND] =
this.addCell(0, 0, toSegmentInfo(aL0, 0, ar));
}
}
// some segment characters matched
// a: ...vvvvvvv
// b: yyyyyyyvvvvvvv
else {
// split current cell
buf32[icell+SEGMENT_INFO] = (bR - br) << 24 | (br - char0);
inext = this.addCell(
buf32[icell+CELL_AND],
0,
(br - bL) << 24 | (bL - char0)
);
// needle remainder: no = need boundary cell
// a: vvvvvvv
// b: yyyyyyyvvvvvvv
// r: yyyyyyy & 0 & vvvvvvv
if ( ar === 0 ) {
iboundary = this.allocateCell();
buf32[icell+CELL_AND] = iboundary;
buf32[iboundary+CELL_AND] = inext;
return iboundary;
}
// needle remainder: yes = need new cell for remaining
// characters
// a: wwwwvvvvvvv
// b: yyyyyyyvvvvvvv
// r: (0 & wwww | yyyyyyy) & vvvvvvv
else {
buf32[icell+CELL_AND] = inext;
iboundary = this.allocateCell();
buf32[inext+CELL_OR] = this.addCell(
iboundary, 0, toSegmentInfo(aL0, 0, ar)
);
return iboundary;
}
}
//debugger; // jshint ignore:line
}
}
getExtra(iboundary) {
return this.buf32[iboundary+BCELL_EXTRA];
}
setExtra(iboundary, v) {
this.buf32[iboundary+BCELL_EXTRA] = v;
}
optimize(shrink = false) {
if ( shrink ) {
this.shrinkBuf();
}
return {
byteLength: this.buf8.byteLength,
char0: this.buf32[CHAR0_SLOT],
};
}
toSelfie() {
const buf32 = this.buf32.subarray(0, this.buf32[CHAR1_SLOT] + 3 >>> 2);
return { buf32, checksum: i32Checksum(buf32) };
}
fromSelfie(selfie) {
if ( typeof selfie !== 'object' || selfie === null ) { return false; }
if ( selfie.buf32 instanceof Uint32Array === false ) { return false; }
if ( selfie.checksum !== i32Checksum(selfie.buf32) ) { return false; }
const byteLength = selfie.buf32.length << 2;
if ( byteLength === 0 ) { return false; }
this.reallocateBuf(byteLength);
this.buf32.set(selfie.buf32);
return true;
}
storeString(s) {
const n = s.length;
if ( n === this.lastStoredLen && s === this.lastStored ) {
return this.lastStoredIndex;
}
this.lastStored = s;
this.lastStoredLen = n;
if ( (this.buf8.length - this.buf32[CHAR1_SLOT]) < n ) {
this.growBuf(0, n);
}
const offset = this.buf32[CHAR1_SLOT];
this.buf32[CHAR1_SLOT] = offset + n;
const buf8 = this.buf8;
for ( let i = 0; i < n; i++ ) {
buf8[offset+i] = s.charCodeAt(i);
}
return (this.lastStoredIndex = offset - this.buf32[CHAR0_SLOT]);
}
extractString(i, n) {
if ( this.textDecoder === null ) {
this.textDecoder = new TextDecoder();
}
const offset = this.buf32[CHAR0_SLOT] + i;
return this.textDecoder.decode(
this.buf8.subarray(offset, offset + n)
);
}
// WASMable.
startsWith(haystackLeft, haystackRight, needleLeft, needleLen) {
if ( haystackLeft < 0 || (haystackLeft + needleLen) > haystackRight ) {
return 0;
}
const charCodes = this.buf8;
needleLeft += this.buf32[CHAR0_SLOT];
const needleRight = needleLeft + needleLen;
while ( charCodes[haystackLeft] === charCodes[needleLeft] ) {
needleLeft += 1;
if ( needleLeft === needleRight ) { return 1; }
haystackLeft += 1;
}
return 0;
}
// Find the left-most instance of substring in main string
// WASMable.
indexOf(haystackLeft, haystackEnd, needleLeft, needleLen) {
if ( needleLen === 0 ) { return haystackLeft; }
haystackEnd -= needleLen;
if ( haystackEnd < haystackLeft ) { return -1; }
needleLeft += this.buf32[CHAR0_SLOT];
const needleRight = needleLeft + needleLen;
const charCodes = this.buf8;
for (;;) {
let i = haystackLeft;
let j = needleLeft;
while ( charCodes[i] === charCodes[j] ) {
j += 1;
if ( j === needleRight ) { return haystackLeft; }
i += 1;
}
haystackLeft += 1;
if ( haystackLeft > haystackEnd ) { break; }
}
return -1;
}
// Find the right-most instance of substring in main string.
// WASMable.
lastIndexOf(haystackBeg, haystackEnd, needleLeft, needleLen) {
if ( needleLen === 0 ) { return haystackBeg; }
let haystackLeft = haystackEnd - needleLen;
if ( haystackLeft < haystackBeg ) { return -1; }
needleLeft += this.buf32[CHAR0_SLOT];
const needleRight = needleLeft + needleLen;
const charCodes = this.buf8;
for (;;) {
let i = haystackLeft;
let j = needleLeft;
while ( charCodes[i] === charCodes[j] ) {
j += 1;
if ( j === needleRight ) { return haystackLeft; }
i += 1;
}
if ( haystackLeft === haystackBeg ) { break; }
haystackLeft -= 1;
}
return -1;
}
dumpTrie(iroot) {
for ( const s of this.trieIterator(iroot) ) {
console.log(s);
}
}
trieIterator(iroot) {
return {
value: undefined,
done: false,
next() {
if ( this.icell === 0 ) {
if ( this.forks.length === 0 ) {
this.value = undefined;
this.done = true;
return this;
}
this.pattern = this.forks.pop();
this.dir = this.forks.pop();
this.icell = this.forks.pop();
}
const buf32 = this.container.buf32;
const buf8 = this.container.buf8;
for (;;) {
const ialt = buf32[this.icell+CELL_OR];
const v = buf32[this.icell+SEGMENT_INFO];
const offset = v & 0x00FFFFFF;
let i0 = buf32[CHAR0_SLOT] + offset;
const len = v >>> 24;
for ( let i = 0; i < len; i++ ) {
this.charBuf[i] = buf8[i0+i];
}
if ( len !== 0 && ialt !== 0 ) {
this.forks.push(ialt, this.dir, this.pattern);
}
const inext = buf32[this.icell+CELL_AND];
if ( len !== 0 ) {
const s = this.textDecoder.decode(
new Uint8Array(this.charBuf.buffer, 0, len)
);
if ( this.dir > 0 ) {
this.pattern += s;
} else if ( this.dir < 0 ) {
this.pattern = s + this.pattern;
}
}
this.icell = inext;
if ( len !== 0 ) { continue; }
// boundary cell
if ( ialt !== 0 ) {
if ( inext === 0 ) {
this.icell = ialt;
this.dir = -1;
} else {
this.forks.push(ialt, -1, this.pattern);
}
}
if ( offset !== 0 ) {
this.value = { pattern: this.pattern, iextra: offset };
return this;
}
}
},
container: this,
icell: iroot,
charBuf: new Uint8Array(256),
pattern: '',
dir: 1,
forks: [],
textDecoder: new TextDecoder(),
[Symbol.iterator]() { return this; },
};
}
async enableWASM(wasmModuleFetcher, path) {
if ( typeof WebAssembly !== 'object' ) { return false; }
if ( this.wasmMemory instanceof WebAssembly.Memory ) { return true; }
const module = await getWasmModule(wasmModuleFetcher, path);
if ( module instanceof WebAssembly.Module === false ) { return false; }
const memory = new WebAssembly.Memory({
initial: roundToPageSize(this.buf8.length) >>> 16
});
const instance = await WebAssembly.instantiate(module, {
imports: { memory, extraHandler: this.extraHandler }
});
if ( instance instanceof WebAssembly.Instance === false ) {
return false;
}
this.wasmMemory = memory;
const curPageCount = memory.buffer.byteLength >>> 16;
const newPageCount = roundToPageSize(this.buf8.byteLength) >>> 16;
if ( newPageCount > curPageCount ) {
memory.grow(newPageCount - curPageCount);
}
const buf8 = new Uint8Array(memory.buffer);
buf8.set(this.buf8);
this.buf8 = buf8;
this.buf32 = new Uint32Array(this.buf8.buffer);
this.haystack = this.buf8.subarray(
HAYSTACK_START,
HAYSTACK_START + HAYSTACK_SIZE
);
this.matches = instance.exports.matches;
this.startsWith = instance.exports.startsWith;
this.indexOf = instance.exports.indexOf;
this.lastIndexOf = instance.exports.lastIndexOf;
return true;
}
dumpInfo() {
return [
`Buffer size (Uint8Array): ${this.buf32[CHAR1_SLOT].toLocaleString('en')}`,
`WASM: ${this.wasmMemory === null ? 'disabled' : 'enabled'}`,
].join('\n');
}
//--------------------------------------------------------------------------
// Private methods
//--------------------------------------------------------------------------
allocateCell() {
let icell = this.buf32[TRIE1_SLOT];
this.buf32[TRIE1_SLOT] = icell + CELL_BYTE_LENGTH;
icell >>>= 2;
this.buf32[icell+0] = 0;
this.buf32[icell+1] = 0;
this.buf32[icell+2] = 0;
return icell;
}
addCell(iand, ior, v) {
const icell = this.allocateCell();
this.buf32[icell+CELL_AND] = iand;
this.buf32[icell+CELL_OR] = ior;
this.buf32[icell+SEGMENT_INFO] = v;
return icell;
}
growBuf(trieGrow, charGrow) {
const char0 = Math.max(
roundToPageSize(this.buf32[TRIE1_SLOT] + trieGrow),
this.buf32[CHAR0_SLOT]
);
const char1 = char0 + this.buf32[CHAR1_SLOT] - this.buf32[CHAR0_SLOT];
const bufLen = Math.max(
roundToPageSize(char1 + charGrow),
this.buf8.length
);
if ( bufLen > this.buf8.length ) {
this.reallocateBuf(bufLen);
}
if ( char0 !== this.buf32[CHAR0_SLOT] ) {
this.buf8.copyWithin(
char0,
this.buf32[CHAR0_SLOT],
this.buf32[CHAR1_SLOT]
);
this.buf32[CHAR0_SLOT] = char0;
this.buf32[CHAR1_SLOT] = char1;
}
}
shrinkBuf() {
const char0 = this.buf32[TRIE1_SLOT] + MIN_FREE_CELL_BYTE_LENGTH;
const char1 = char0 + this.buf32[CHAR1_SLOT] - this.buf32[CHAR0_SLOT];
const bufLen = char1 + 256;
if ( char0 !== this.buf32[CHAR0_SLOT] ) {
this.buf8.copyWithin(
char0,
this.buf32[CHAR0_SLOT],
this.buf32[CHAR1_SLOT]
);
this.buf32[CHAR0_SLOT] = char0;
this.buf32[CHAR1_SLOT] = char1;
}
if ( bufLen < this.buf8.length ) {
this.reallocateBuf(bufLen);
}
}
reallocateBuf(newSize) {
newSize = roundToPageSize(newSize);
if ( newSize === this.buf8.length ) { return; }
if ( this.wasmMemory === null ) {
const newBuf = new Uint8Array(newSize);
newBuf.set(
newBuf.length < this.buf8.length
? this.buf8.subarray(0, newBuf.length)
: this.buf8
);
this.buf8 = newBuf;
} else {
const growBy =
((newSize + 0xFFFF) >>> 16) - (this.buf8.length >>> 16);
if ( growBy <= 0 ) { return; }
this.wasmMemory.grow(growBy);
this.buf8 = new Uint8Array(this.wasmMemory.buffer);
}
this.buf32 = new Uint32Array(this.buf8.buffer);
this.haystack = this.buf8.subarray(
HAYSTACK_START,
HAYSTACK_START + HAYSTACK_SIZE
);
}
}
/******************************************************************************/
// Code below is to attempt to load a WASM module which implements:
//
// - BidiTrieContainer.startsWith()
//
// The WASM module is entirely optional, the JS implementations will be
// used should the WASM module be unavailable for whatever reason.
const getWasmModule = (( ) => {
let wasmModulePromise;
return async function(wasmModuleFetcher, path) {
if ( wasmModulePromise instanceof Promise ) {
return wasmModulePromise;
}
if ( typeof WebAssembly !== 'object' ) { return; }
// Soft-dependency on vAPI so that the code here can be used outside of
// uBO (i.e. tests, benchmarks)
if ( typeof vAPI === 'object' && vAPI.canWASM !== true ) { return; }
// The wasm module will work only if CPU is natively little-endian,
// as we use native uint32 array in our js code.
const uint32s = new Uint32Array(1);
const uint8s = new Uint8Array(uint32s.buffer);
uint32s[0] = 1;
if ( uint8s[0] !== 1 ) { return; }
wasmModulePromise = wasmModuleFetcher(`${path}biditrie`).catch(reason => {
console.info(reason);
});
return wasmModulePromise;
};
})();
/******************************************************************************/
export default BidiTrieContainer;

View File

@@ -0,0 +1,85 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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
*/
import webext from './webext.js';
/******************************************************************************/
// Broadcast a message to all uBO contexts
let broadcastChannel;
export function broadcast(message) {
if ( broadcastChannel === undefined ) {
broadcastChannel = new self.BroadcastChannel('uBO');
}
broadcastChannel.postMessage(message);
}
/******************************************************************************/
// Broadcast a message to all uBO contexts and all uBO's content scripts
export async function broadcastToAll(message) {
broadcast(message);
const tabs = await vAPI.tabs.query({
discarded: false,
});
const bcmessage = Object.assign({ broadcast: true }, message);
for ( const tab of tabs ) {
webext.tabs.sendMessage(tab.id, bcmessage).catch(( ) => { });
}
}
/******************************************************************************/
export function onBroadcast(listener) {
const bc = new self.BroadcastChannel('uBO');
bc.onmessage = ev => listener(ev.data || {});
return bc;
}
/******************************************************************************/
export function filteringBehaviorChanged(details = {}) {
if ( typeof details.direction !== 'number' || details.direction >= 0 ) {
filteringBehaviorChanged.throttle.offon(727);
}
broadcast(Object.assign({ what: '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
);
/******************************************************************************/

View File

@@ -0,0 +1,731 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2016-present The uBlock Origin authors
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
*/
/******************************************************************************/
import * as s14e from './s14e-serializer.js';
import { ubolog } from './console.js';
import webext from './webext.js';
import µb from './background.js';
/******************************************************************************/
const STORAGE_NAME = 'uBlock0CacheStorage';
const extensionStorage = webext.storage.local;
const pendingWrite = new Map();
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);
};
let fastCache = 'indexedDB';
// https://eslint.org/docs/latest/rules/no-prototype-builtins
const hasOwnProperty = (o, p) =>
Object.prototype.hasOwnProperty.call(o, p);
/*******************************************************************************
*
* Extension storage
*
* Always available.
*
* */
const cacheStorage = (( ) => {
const exGet = async (api, wanted, outbin) => {
ubolog('cacheStorage.get:', api.name || 'storage.local', wanted.join());
const missing = [];
for ( const key of wanted ) {
if ( pendingWrite.has(key) ) {
outbin[key] = pendingWrite.get(key);
} else {
missing.push(key);
}
}
if ( missing.length === 0 ) { return; }
return api.get(missing).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 ( hasOwnProperty(outbin, 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 ( hasOwnProperty(argbin, key) === false ) { continue; }
outbin[key] = argbin[key];
}
}).then(( ) => {
const promises = [];
for ( const key of Object.keys(outbin) ) {
promises.push(decompress(outbin, key));
}
return Promise.all(promises).then(( ) => outbin);
}).catch(reason => {
ubolog(reason);
});
},
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; }
ubolog('cacheStorage.set:', keys.join());
for ( const key of keys ) {
pendingWrite.set(key, rawbin[key]);
}
try {
const serializedbin = {};
const promises = [];
for ( const key of keys ) {
promises.push(compress(serializedbin, key, rawbin[key]));
}
await Promise.all(promises);
await Promise.all([
cacheAPIs[fastCache].set(rawbin, serializedbin),
extensionStorage.set(serializedbin),
]);
} catch(reason) {
ubolog(reason);
}
for ( const key of keys ) {
pendingWrite.delete(key);
}
},
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 ( hasOwnProperty(cacheAPIs, api) === false ) { return fastCache; }
fastCache = api;
for ( const k of Object.keys(cacheAPIs) ) {
if ( k === api ) { continue; }
cacheAPIs[k]['clear']();
}
return fastCache;
},
};
// Not all platforms support getBytesInUse
if ( extensionStorage.getBytesInUse instanceof Function ) {
api.getBytesInUse = function(...args) {
return extensionStorage.getBytesInUse(...args).catch(reason => {
ubolog(reason);
});
};
}
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 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 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 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);
});
};
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 removeOne = async key => {
const cache = await getAPI();
if ( cache === null ) { return; }
return cache.delete(keyToURL(key)).catch(reason => {
ubolog(reason);
});
};
return {
name: 'cacheAPI',
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];
}
toFetch[i] = getOne(key);
}
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;
}
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 {
name: 'memoryStorage',
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;
db.createObjectStore(STORAGE_NAME, { keyPath: 'key' });
} catch(ex) {
req.onerror();
}
};
req.onsuccess = ev => {
if ( resolve === undefined ) { return; }
resolve(ev.target.result || null);
resolve = undefined;
};
req.onerror = req.onblocked = ( ) => {
if ( resolve === undefined ) { return; }
ubolog(req.error);
resolve(null);
resolve = undefined;
};
vAPI.defer.once(10000).then(( ) => {
if ( resolve === undefined ) { return; }
resolve(null);
resolve = undefined;
});
}).catch(reason => {
ubolog(`idbStorage() / getDb() failed: ${reason}`);
return null;
});
return dbPromise;
};
// Cache API is subject to quota so we will use it only for what is key
// performance-wise
const shouldCache = key => {
if ( key.startsWith('cache/') === false ) { return true; }
return /^cache\/(compiled|selfie)\//.test(key);
};
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 === false ) {
entries.push({ key, value });
}
cursor.continue();
};
}).catch(reason => {
ubolog(`idbStorage() / getAllEntries() failed: ${reason}`);
return [];
});
};
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 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 ) { return; }
entries.push({ key, value });
};
const transaction = db.transaction(STORAGE_NAME, 'readonly');
transaction.oncomplete =
transaction.onerror =
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 = ( ) => { };
}
}).catch(reason => {
ubolog(`idbStorage() / getEntries() failed: ${reason}`);
return [];
});
};
const getAll = async ( ) => {
const entries = await getAllEntries();
const outbin = {};
for ( const { key, value } of entries ) {
outbin[key] = value;
}
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] });
}
const transaction = db.transaction(STORAGE_NAME, 'readwrite');
transaction.oncomplete =
transaction.onerror =
transaction.onabort = ( ) => {
resolve();
};
const table = transaction.objectStore(STORAGE_NAME);
for ( const entry of entries ) {
table.put(entry);
}
}).catch(reason => {
ubolog(`idbStorage() / setEntries() failed: ${reason}`);
});
};
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 = ( ) => {
resolve();
};
const table = transaction.objectStore(STORAGE_NAME);
for ( const key of keys ) {
table.delete(key);
}
}).catch(reason => {
ubolog(`idbStorage() / deleteEntries() failed: ${reason}`);
});
};
return {
name: 'idbStorage',
async get(argbin) {
const keys = keysFromGetArg(argbin);
if ( keys === undefined ) { return; }
if ( keys.length === 0 ) { return getAll(); }
const entries = await getEntries(keys);
const outbin = {};
const toRemove = [];
for ( const { key, value } of entries ) {
if ( shouldCache(key) === false ) {
toRemove.push(key);
continue;
}
outbin[key] = value;
}
if ( argbin instanceof Object && Array.isArray(argbin) === false ) {
for ( const key of keys ) {
if ( hasOwnProperty(outbin, key) ) { continue; }
outbin[key] = argbin[key];
}
}
if ( toRemove.length !== 0 ) {
deleteEntries(toRemove);
}
return outbin;
},
async set(rawbin) {
const bin = {};
for ( const key of Object.keys(rawbin) ) {
if ( shouldCache(key) === false ) { continue; }
bin[key] = rawbin[key];
}
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;
},
};
})();
/******************************************************************************/
const cacheAPIs = {
'indexedDB': idbStorage,
'cacheAPI': cacheAPI,
'browser.storage.session': memoryStorage,
};
/******************************************************************************/
export default cacheStorage;
/******************************************************************************/

View File

@@ -0,0 +1,59 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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 ( typeof vAPI !== 'object' ) { return; }
const url = new URL(self.location.href);
const actualURL = url.searchParams.get('url');
const frameURL = url.searchParams.get('aliasURL') || actualURL;
const frameURLElem = document.getElementById('frameURL');
frameURLElem.children[0].textContent = actualURL;
frameURLElem.children[1].href = frameURL;
frameURLElem.children[1].title = frameURL;
document.body.setAttribute('title', actualURL);
document.body.addEventListener('click', ev => {
if ( ev.isTrusted === false ) { return; }
if ( ev.target.closest('#frameURL') !== null ) { return; }
vAPI.messaging.send('default', {
what: 'clickToLoad',
frameURL,
}).then(ok => {
if ( ok !== true ) { return; }
self.location.replace(frameURL);
});
});
/******************************************************************************/
})();

View File

@@ -0,0 +1,238 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2015-2018 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
*/
/* global faIconsInit */
'use strict';
import { i18n, i18n$ } from './i18n.js';
import { dom, qs$ } from './dom.js';
import { faIconsInit } from './fa-icons.js';
/******************************************************************************/
(( ) => {
/******************************************************************************/
self.cloud = {
options: {},
datakey: '',
data: undefined,
onPush: null,
onPull: null,
};
/******************************************************************************/
const widget = qs$('#cloudWidget');
if ( widget === null ) { return; }
self.cloud.datakey = dom.attr(widget, 'data-cloud-entry') || '';
if ( self.cloud.datakey === '' ) { return; }
/******************************************************************************/
const fetchStorageUsed = async function() {
let elem = qs$(widget, '#cloudCapacity');
if ( dom.cl.has(elem, 'hide') ) { return; }
const result = await vAPI.messaging.send('cloudWidget', {
what: 'cloudUsed',
datakey: self.cloud.datakey,
});
if ( result instanceof Object === false ) {
dom.cl.add(elem, 'hide');
return;
}
const units = ' ' + i18n$('genericBytes');
elem.title = result.max.toLocaleString() + units;
const total = (result.total / result.max * 100).toFixed(1);
elem = elem.firstElementChild;
elem.style.width = `${total}%`;
elem.title = result.total.toLocaleString() + units;
const used = (result.used / result.total * 100).toFixed(1);
elem = elem.firstElementChild;
elem.style.width = `${used}%`;
elem.title = result.used.toLocaleString() + units;
};
/******************************************************************************/
const fetchCloudData = async function() {
const info = qs$(widget, '#cloudInfo');
const entry = await vAPI.messaging.send('cloudWidget', {
what: 'cloudPull',
datakey: self.cloud.datakey,
});
const hasData = entry instanceof Object;
if ( hasData === false ) {
dom.attr('#cloudPull', 'disabled', '');
dom.attr('#cloudPullAndMerge', 'disabled', '');
info.textContent = '...\n...';
return entry;
}
self.cloud.data = entry.data;
dom.attr('#cloudPull', 'disabled', null);
dom.attr('#cloudPullAndMerge', 'disabled', null);
const timeOptions = {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZoneName: 'short'
};
const time = new Date(entry.tstamp);
info.textContent =
entry.source + '\n' +
time.toLocaleString('fullwide', timeOptions);
};
/******************************************************************************/
const pushData = async function() {
if ( typeof self.cloud.onPush !== 'function' ) { return; }
const error = await vAPI.messaging.send('cloudWidget', {
what: 'cloudPush',
datakey: self.cloud.datakey,
data: self.cloud.onPush(),
});
const failed = typeof error === 'string';
dom.cl.toggle('#cloudPush', 'error', failed);
dom.text('#cloudError', failed ? error : '');
if ( failed ) { return; }
fetchCloudData();
fetchStorageUsed();
};
/******************************************************************************/
const pullData = function() {
if ( typeof self.cloud.onPull === 'function' ) {
self.cloud.onPull(self.cloud.data, false);
}
dom.cl.remove('#cloudPush', 'error');
dom.text('#cloudError', '');
};
/******************************************************************************/
const pullAndMergeData = function() {
if ( typeof self.cloud.onPull === 'function' ) {
self.cloud.onPull(self.cloud.data, true);
}
};
/******************************************************************************/
const openOptions = function() {
const input = qs$('#cloudDeviceName');
input.value = self.cloud.options.deviceName;
dom.attr(input, 'placeholder', self.cloud.options.defaultDeviceName);
dom.cl.add('#cloudOptions', 'show');
};
/******************************************************************************/
const closeOptions = function(ev) {
const root = qs$('#cloudOptions');
if ( ev.target !== root ) { return; }
dom.cl.remove(root, 'show');
};
/******************************************************************************/
const submitOptions = async function() {
dom.cl.remove('#cloudOptions', 'show');
const options = await vAPI.messaging.send('cloudWidget', {
what: 'cloudSetOptions',
options: {
deviceName: qs$('#cloudDeviceName').value
},
});
if ( options instanceof Object ) {
self.cloud.options = options;
}
};
/******************************************************************************/
const onInitialize = function(options) {
if ( options instanceof Object === false ) { return; }
if ( options.enabled !== true ) { return; }
self.cloud.options = options;
const xhr = new XMLHttpRequest();
xhr.open('GET', 'cloud-ui.html', true);
xhr.overrideMimeType('text/html;charset=utf-8');
xhr.responseType = 'text';
xhr.onload = function() {
this.onload = null;
const parser = new DOMParser(),
parsed = parser.parseFromString(this.responseText, 'text/html'),
fromParent = parsed.body;
while ( fromParent.firstElementChild !== null ) {
widget.appendChild(
document.adoptNode(fromParent.firstElementChild)
);
}
faIconsInit(widget);
i18n.render(widget);
dom.cl.remove(widget, 'hide');
dom.on('#cloudPush', 'click', ( ) => { pushData(); });
dom.on('#cloudPull', 'click', pullData);
dom.on('#cloudPullAndMerge', 'click', pullAndMergeData);
dom.on('#cloudCog', 'click', openOptions);
dom.on('#cloudOptions', 'click', closeOptions);
dom.on('#cloudOptionsSubmit', 'click', ( ) => { submitOptions(); });
fetchCloudData().then(result => {
if ( typeof result !== 'string' ) { return; }
dom.cl.add('#cloudPush', 'error');
dom.text('#cloudError', result);
});
fetchStorageUsed();
};
xhr.send();
};
vAPI.messaging.send('cloudWidget', {
what: 'cloudGetOptions',
}).then(options => {
onInitialize(options);
});
/******************************************************************************/
})();

View File

@@ -0,0 +1,311 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2023-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
*/
/* globals CodeMirror, uBlockDashboard, beautifier */
'use strict';
/******************************************************************************/
import { dom, qs$ } from './dom.js';
import { getActualTheme } from './theme.js';
/******************************************************************************/
const urlToDocMap = new Map();
const params = new URLSearchParams(document.location.search);
let currentURL = '';
const cmEditor = new CodeMirror(qs$('#content'), {
autofocus: true,
gutters: [ 'CodeMirror-linenumbers' ],
lineNumbers: true,
lineWrapping: true,
matchBrackets: true,
styleActiveLine: {
nonEmpty: true,
},
});
uBlockDashboard.patchCodeMirrorEditor(cmEditor);
vAPI.messaging.send('dom', { what: 'uiStyles' }).then(response => {
if ( typeof response !== 'object' || response === null ) { return; }
if ( getActualTheme(response.uiTheme) === 'dark' ) {
dom.cl.add('#content .cm-s-default', 'cm-s-night');
dom.cl.remove('#content .cm-s-default', 'cm-s-default');
}
});
// Convert resource URLs into clickable links to code viewer
cmEditor.addOverlay({
re: /\b(?:href|src)=["']([^"']+)["']/g,
match: null,
token: function(stream) {
if ( stream.sol() ) {
this.re.lastIndex = 0;
this.match = this.re.exec(stream.string);
}
if ( this.match === null ) {
stream.skipToEnd();
return null;
}
const end = this.re.lastIndex - 1;
const beg = end - this.match[1].length;
if ( stream.pos < beg ) {
stream.pos = beg;
return null;
}
if ( stream.pos < end ) {
stream.pos = end;
return 'href';
}
if ( stream.pos < this.re.lastIndex ) {
stream.pos = this.re.lastIndex;
this.match = this.re.exec(stream.string);
return null;
}
stream.skipToEnd();
return null;
},
});
urlToDocMap.set('', cmEditor.getDoc());
/******************************************************************************/
async function fetchResource(url) {
let response, text;
const fetchOptions = {
method: 'GET',
referrer: '',
};
if ( urlToDocMap.has(url) ) {
fetchOptions.cache = 'reload';
}
try {
response = await fetch(url, fetchOptions);
text = await response.text();
} catch(reason) {
text = String(reason);
}
let mime = response && response.headers.get('Content-Type') || '';
mime = mime.replace(/\s*;.*$/, '').trim();
const beautifierOptions = {
end_with_newline: true,
indent_size: 3,
js: {
max_preserve_newlines: 3,
}
};
switch ( mime ) {
case 'text/css':
text = beautifier.css(text, beautifierOptions);
break;
case 'text/html':
case 'application/xhtml+xml':
case 'application/xml':
case 'image/svg+xml':
text = beautifier.html(text, beautifierOptions);
break;
case 'text/javascript':
case 'application/javascript':
case 'application/x-javascript':
text = beautifier.js(text, beautifierOptions);
break;
case 'application/json':
text = beautifier.js(text, beautifierOptions);
break;
default:
break;
}
return { mime, text };
}
/******************************************************************************/
function addPastURLs(url) {
const list = qs$('#pastURLs');
let current;
for ( let i = 0; i < list.children.length; i++ ) {
const span = list.children[i];
dom.cl.remove(span, 'selected');
if ( span.textContent !== url ) { continue; }
current = span;
}
if ( url === '' ) { return; }
if ( current === undefined ) {
current = document.createElement('span');
current.textContent = url;
list.prepend(current);
}
dom.cl.add(current, 'selected');
}
/******************************************************************************/
function setInputURL(url) {
const input = qs$('#header input[type="url"]');
if ( url === input.value ) { return; }
dom.attr(input, 'value', url);
input.value = url;
}
/******************************************************************************/
async function setURL(resourceURL) {
// For convenience, remove potentially existing quotes around the URL
if ( /^(["']).+\1$/.test(resourceURL) ) {
resourceURL = resourceURL.slice(1, -1);
}
let afterURL;
if ( resourceURL !== '' ) {
try {
const url = new URL(resourceURL, currentURL || undefined);
url.hash = '';
afterURL = url.href;
} catch(ex) {
}
if ( afterURL === undefined ) { return; }
} else {
afterURL = '';
}
if ( afterURL !== '' && /^https?:\/\/./.test(afterURL) === false ) {
return;
}
if ( afterURL === currentURL ) {
if ( afterURL !== resourceURL ) {
setInputURL(afterURL);
}
return;
}
let afterDoc = urlToDocMap.get(afterURL);
if ( afterDoc === undefined ) {
const r = await fetchResource(afterURL) || { mime: '', text: '' };
afterDoc = new CodeMirror.Doc(r.text, r.mime || '');
urlToDocMap.set(afterURL, afterDoc);
}
swapDoc(afterDoc);
currentURL = afterURL;
setInputURL(afterURL);
const a = qs$('.cm-search-widget .sourceURL');
dom.attr(a, 'href', afterURL);
dom.attr(a, 'title', afterURL);
addPastURLs(afterURL);
// For unknown reasons, calling focus() synchronously does not work...
vAPI.defer.once(1).then(( ) => { cmEditor.focus(); });
}
/******************************************************************************/
function removeURL(url) {
if ( url === '' ) { return; }
const list = qs$('#pastURLs');
let foundAt = -1;
for ( let i = 0; i < list.children.length; i++ ) {
const span = list.children[i];
if ( span.textContent !== url ) { continue; }
foundAt = i;
}
if ( foundAt === -1 ) { return; }
list.children[foundAt].remove();
if ( foundAt >= list.children.length ) {
foundAt = list.children.length - 1;
}
const afterURL = foundAt !== -1
? list.children[foundAt].textContent
: '';
setURL(afterURL);
urlToDocMap.delete(url);
}
/******************************************************************************/
function swapDoc(doc) {
const r = cmEditor.swapDoc(doc);
if ( self.searchThread ) {
self.searchThread.setHaystack(cmEditor.getValue());
}
const input = qs$('.cm-search-widget-input input[type="search"]');
if ( input.value !== '' ) {
qs$('.cm-search-widget').dispatchEvent(new Event('input'));
}
return r;
}
/******************************************************************************/
async function start() {
await setURL(params.get('url'));
dom.on('#header input[type="url"]', 'change', ev => {
setURL(ev.target.value);
});
dom.on('#reloadURL', 'click', ( ) => {
const input = qs$('#header input[type="url"]');
const url = input.value;
const beforeDoc = swapDoc(new CodeMirror.Doc('', ''));
fetchResource(url).then(r => {
if ( urlToDocMap.has(url) === false ) { return; }
const afterDoc = r !== undefined
? new CodeMirror.Doc(r.text, r.mime || '')
: beforeDoc;
urlToDocMap.set(url, afterDoc);
if ( currentURL !== url ) { return; }
swapDoc(afterDoc);
});
});
dom.on('#removeURL', 'click', ( ) => {
removeURL(qs$('#header input[type="url"]').value);
});
dom.on('#pastURLs', 'mousedown', 'span', ev => {
setURL(ev.target.textContent);
});
dom.on('#content', 'click', '.cm-href', ev => {
const target = ev.target;
const urlParts = [ target.textContent ];
let previous = target;
for (;;) {
previous = previous.previousSibling;
if ( previous === null ) { break; }
if ( previous.nodeType !== 1 ) { break; }
if ( previous.classList.contains('cm-href') === false ) { break; }
urlParts.unshift(previous.textContent);
}
let next = target;
for (;;) {
next = next.nextSibling;
if ( next === null ) { break; }
if ( next.nodeType !== 1 ) { break; }
if ( next.classList.contains('cm-href') === false ) { break; }
urlParts.push(next.textContent);
}
setURL(urlParts.join(''));
});
}
start();
/******************************************************************************/

View File

@@ -0,0 +1,199 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2020-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';
/******************************************************************************/
(( ) => {
// >>>>> start of local scope
/******************************************************************************/
// Worker context
if (
self.WorkerGlobalScope instanceof Object &&
self instanceof self.WorkerGlobalScope
) {
let content = '';
const doSearch = function(details) {
const reEOLs = /\n\r|\r\n|\n|\r/g;
const t1 = Date.now() + 750;
let reSearch;
try {
reSearch = new RegExp(details.pattern, details.flags);
} catch(ex) {
return;
}
const response = [];
const maxOffset = content.length;
let iLine = 0;
let iOffset = 0;
let size = 0;
while ( iOffset < maxOffset ) {
// Find next match
const match = reSearch.exec(content);
if ( match === null ) { break; }
// Find number of line breaks between last and current match.
reEOLs.lastIndex = 0;
const eols = content.slice(iOffset, match.index).match(reEOLs);
if ( Array.isArray(eols) ) {
iLine += eols.length;
}
// Store line
response.push(iLine);
size += 1;
// Find next line break.
reEOLs.lastIndex = reSearch.lastIndex;
const eol = reEOLs.exec(content);
iOffset = eol !== null
? reEOLs.lastIndex
: content.length;
reSearch.lastIndex = iOffset;
iLine += 1;
// Quit if this takes too long
if ( (size & 0x3FF) === 0 && Date.now() >= t1 ) { break; }
}
return response;
};
self.onmessage = function(e) {
const msg = e.data;
switch ( msg.what ) {
case 'setHaystack':
content = msg.content;
break;
case 'doSearch':
const response = doSearch(msg);
self.postMessage({ id: msg.id, response });
break;
}
};
return;
}
/******************************************************************************/
// Main context
{
const workerTTL = { min: 5 };
const pendingResponses = new Map();
const workerTTLTimer = vAPI.defer.create(( ) => {
shutdown();
});
let worker;
let messageId = 1;
const onWorkerMessage = function(e) {
const msg = e.data;
const resolver = pendingResponses.get(msg.id);
if ( resolver === undefined ) { return; }
pendingResponses.delete(msg.id);
resolver(msg.response);
};
const cancelPendingTasks = function() {
for ( const resolver of pendingResponses.values() ) {
resolver();
}
pendingResponses.clear();
};
const destroy = function() {
shutdown();
self.searchThread = undefined;
};
const shutdown = function() {
if ( worker === undefined ) { return; }
workerTTLTimer.off();
worker.terminate();
worker.onmessage = undefined;
worker = undefined;
cancelPendingTasks();
};
const init = function() {
if ( self.searchThread instanceof Object === false ) { return; }
if ( worker === undefined ) {
worker = new Worker('js/codemirror/search-thread.js');
worker.onmessage = onWorkerMessage;
}
workerTTLTimer.offon(workerTTL);
};
const needHaystack = function() {
return worker instanceof Object === false;
};
const setHaystack = function(content) {
init();
worker.postMessage({ what: 'setHaystack', content });
};
const search = function(query, overwrite = true) {
init();
if ( worker instanceof Object === false ) {
return Promise.resolve();
}
if ( overwrite ) {
cancelPendingTasks();
}
const id = messageId++;
worker.postMessage({
what: 'doSearch',
id,
pattern: query.source,
flags: query.flags,
isRE: query instanceof RegExp
});
return new Promise(resolve => {
pendingResponses.set(id, resolve);
});
};
self.addEventListener(
'beforeunload',
( ) => { destroy(); },
{ once: true }
);
self.searchThread = { needHaystack, setHaystack, search, shutdown };
}
/******************************************************************************/
// <<<<< end of local scope
})();
/******************************************************************************/
void 0;

View File

@@ -0,0 +1,516 @@
// The following code is heavily based on the standard CodeMirror
// search addon found at: https://codemirror.net/addon/search/search.js
// I added/removed and modified code in order to get a closer match to a
// browser's built-in find-in-page feature which are just enough for
// uBlock Origin.
//
// This file was originally wholly imported from:
// https://github.com/codemirror/CodeMirror/blob/3e1bb5fff682f8f6cbfaef0e56c61d62403d4798/addon/search/search.js
//
// And has been modified over time to better suit uBO's usage and coding style:
// https://github.com/gorhill/uBlock/commits/master/src/js/codemirror/search.js
//
// The original copyright notice is reproduced below:
// =====
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// Define search commands. Depends on dialog.js or another
// implementation of the openDialog method.
// Replace works a little oddly -- it will do the replace on the next
// Ctrl-G (or whatever is bound to findNext) press. You prevent a
// replace by making sure the match is no longer selected when hitting
// Ctrl-G.
// =====
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, '\\$&'),
caseInsensitive ? 'gi' : 'g'
);
else if ( !query.global )
query = new RegExp(query.source, query.ignoreCase ? 'gi' : 'g');
return {
token: function(stream) {
query.lastIndex = stream.pos;
const match = query.exec(stream.string);
if ( match && match.index === stream.pos ) {
stream.pos += match[0].length || 1;
return 'searching';
} else if ( match ) {
stream.pos = match.index;
} else {
stream.skipToEnd();
}
}
};
};
const searchWidgetKeydownHandler = function(cm, ev) {
const keyName = CodeMirror.keyName(ev);
if ( !keyName ) { return; }
CodeMirror.lookupKey(
keyName,
cm.getOption('keyMap'),
function(command) {
if ( widgetCommandHandler(cm, command) ) {
ev.preventDefault();
ev.stopPropagation();
}
}
);
};
const searchWidgetInputHandler = function(cm, ev) {
const state = getSearchState(cm);
if ( ev.isTrusted !== true ) {
if ( state.queryText === '' ) {
clearSearch(cm);
} else {
cm.operation(function() {
startSearch(cm, state);
});
}
return;
}
if ( queryTextFromSearchWidget(cm) === state.queryText ) { return; }
state.queryTimer.offon(350);
};
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') ) {
findNext(cm, 1);
} else if ( tcl.contains('cm-linter-widget-up') ) {
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 ( target.localName !== 'input' ) {
cm.focus();
}
};
const queryTextFromSearchWidget = function(cm) {
return getSearchState(cm).widget.querySelector('input[type="search"]').value;
};
const queryTextToSearchWidget = function(cm, q) {
const input = getSearchState(cm).widget.querySelector('input[type="search"]');
if ( typeof q === 'string' && q !== input.value ) {
input.value = q;
}
input.setSelectionRange(0, input.value.length);
input.focus();
};
const SearchState = function(cm) {
this.query = null;
this.panel = null;
const widgetParent = document.querySelector('.cm-search-widget-template').cloneNode(true);
this.widget = widgetParent.children[0];
this.widget.addEventListener('keydown', searchWidgetKeydownHandler.bind(null, cm));
this.widget.addEventListener('input', searchWidgetInputHandler.bind(null, cm));
this.widget.addEventListener('click', ev => {
searchWidgetClickHandler(ev, cm);
});
if ( typeof cm.addPanel === 'function' ) {
this.panel = cm.addPanel(this.widget);
}
this.queryText = '';
this.dirty = true;
this.lines = [];
cm.on('changes', (cm, changes) => {
for ( const change of changes ) {
if ( change.text.length !== 0 || change.removed !== 0 ) {
this.dirty = true;
break;
}
}
});
cm.on('cursorActivity', cm => {
updateCount(cm);
});
this.queryTimer = vAPI.defer.create(( ) => {
findCommit(cm, 0);
});
};
// We want the search widget to behave as if the focus was on the
// CodeMirror editor.
const reSearchCommands = /^(?:find|findNext|findPrev|newlineAndIndent)$/;
const widgetCommandHandler = function(cm, command) {
if ( reSearchCommands.test(command) === false ) { return false; }
const queryText = queryTextFromSearchWidget(cm);
if ( command === 'find' ) {
queryTextToSearchWidget(cm);
return true;
}
if ( queryText.length !== 0 ) {
findNext(cm, command === 'findPrev' ? -1 : 1);
}
return true;
};
const getSearchState = function(cm) {
return cm.state.search || (cm.state.search = new SearchState(cm));
};
const queryCaseInsensitive = function(query) {
return typeof query === 'string' && query === query.toLowerCase();
};
// Heuristic: if the query string is all lowercase, do a case insensitive search.
const getSearchCursor = function(cm, query, pos) {
return cm.getSearchCursor(
query,
pos,
{ caseFold: queryCaseInsensitive(query), multiline: false }
);
};
// https://github.com/uBlockOrigin/uBlock-issues/issues/658
// Modified to backslash-escape ONLY widely-used control characters.
const parseString = function(string) {
return string.replace(/\\[nrt\\]/g, match => {
if ( match === '\\n' ) { return '\n'; }
if ( match === '\\r' ) { return '\r'; }
if ( match === '\\t' ) { return '\t'; }
if ( match === '\\\\' ) { return '\\'; }
return match;
});
};
const reEscape = /[.*+\-?^${}()|[\]\\]/g;
// Must always return a RegExp object.
//
// Assume case-sensitivity if there is at least one uppercase in plain
// query text.
const parseQuery = function(query) {
let flags = 'i';
let reParsed = query.match(/^\/(.+)\/([iu]*)$/);
if ( reParsed !== null ) {
try {
const re = new RegExp(reParsed[1], reParsed[2]);
query = re.source;
flags = re.flags;
}
catch (e) {
reParsed = null;
}
}
if ( reParsed === null ) {
if ( /[A-Z]/.test(query) ) { flags = ''; }
query = parseString(query).replace(reEscape, '\\$&');
}
if ( typeof query === 'string' ? query === '' : query.test('') ) {
query = 'x^';
}
return new RegExp(query, 'gm' + flags);
};
let intlNumberFormat;
const formatNumber = function(n) {
if ( intlNumberFormat === undefined ) {
intlNumberFormat = null;
if ( Intl.NumberFormat instanceof Function ) {
const intl = new Intl.NumberFormat(undefined, {
notation: 'compact',
maximumSignificantDigits: 3
});
if ( intl.resolvedOptions().notation ) {
intlNumberFormat = intl;
}
}
}
return n > 10000 && intlNumberFormat instanceof Object
? intlNumberFormat.format(n)
: n.toLocaleString();
};
const updateCount = function(cm) {
const state = getSearchState(cm);
const lines = state.lines;
const current = cm.getCursor().line;
let l = 0;
let r = lines.length;
let i = -1;
while ( l < r ) {
i = l + r >>> 1;
const candidate = lines[i];
if ( current === candidate ) { break; }
if ( current < candidate ) {
r = i;
} else /* if ( current > candidate ) */ {
l = i + 1;
}
}
let text = '';
if ( i !== -1 ) {
text = formatNumber(i + 1);
if ( lines[i] !== current ) {
text = '~' + text;
}
text = text + '\xA0/\xA0';
}
const count = lines.length;
text += formatNumber(count);
const span = state.widget.querySelector('.cm-search-widget-count');
span.textContent = text;
span.title = count.toLocaleString();
};
const startSearch = function(cm, state) {
state.query = parseQuery(state.queryText);
if ( state.overlay !== undefined ) {
cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
}
state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
cm.addOverlay(state.overlay);
if ( state.dirty || self.searchThread.needHaystack() ) {
self.searchThread.setHaystack(cm.getValue());
state.dirty = false;
}
self.searchThread.search(state.query).then(lines => {
if ( Array.isArray(lines) === false ) { return; }
state.lines = lines;
const count = lines.length;
updateCount(cm);
if ( state.annotate !== undefined ) {
state.annotate.clear();
state.annotate = undefined;
}
if ( count === 0 ) { return; }
state.annotate = cm.annotateScrollbar('CodeMirror-search-match');
const annotations = [];
let lineBeg = -1;
let lineEnd = -1;
for ( const line of lines ) {
if ( lineBeg === -1 ) {
lineBeg = line;
lineEnd = line + 1;
continue;
} else if ( line === lineEnd ) {
lineEnd = line + 1;
continue;
}
annotations.push({
from: { line: lineBeg, ch: 0 },
to: { line: lineEnd, ch: 0 }
});
lineBeg = -1;
}
if ( lineBeg !== -1 ) {
annotations.push({
from: { line: lineBeg, ch: 0 },
to: { line: lineEnd, ch: 0 }
});
}
state.annotate.update(annotations);
});
state.widget.setAttribute('data-query', state.queryText);
};
const findNext = function(cm, dir, callback) {
cm.operation(function() {
const state = getSearchState(cm);
if ( !state.query ) { return; }
let cursor = getSearchCursor(
cm,
state.query,
dir <= 0 ? cm.getCursor('from') : cm.getCursor('to')
);
const previous = dir < 0;
if (!cursor.find(previous)) {
cursor = getSearchCursor(
cm,
state.query,
previous
? CodeMirror.Pos(cm.lastLine())
: CodeMirror.Pos(cm.firstLine(), 0)
);
if (!cursor.find(previous)) return;
}
cm.setSelection(cursor.from(), cursor.to());
const { clientHeight } = cm.getScrollInfo();
cm.scrollIntoView(
{ from: cursor.from(), to: cursor.to() },
clientHeight >>> 1
);
if (callback) callback(cursor.from(), cursor.to());
});
};
const findNextError = function(cm, dir) {
const doc = cm.getDoc();
const cursor = cm.getCursor('from');
const cursorLine = cursor.line;
const start = dir < 0 ? 0 : cursorLine + 1;
const end = dir < 0 ? cursorLine : doc.lineCount();
let found = -1;
doc.eachLine(start, end, lineHandle => {
const markers = lineHandle.gutterMarkers || null;
if ( markers === null ) { return; }
const marker = markers['CodeMirror-lintgutter'];
if ( marker === undefined ) { return; }
if ( marker.dataset.error !== 'y' ) { return; }
const line = lineHandle.lineNo();
if ( dir < 0 ) {
found = line;
return;
}
found = line;
return true;
});
if ( found === -1 || found === cursorLine ) { return; }
cm.getDoc().setCursor(found);
const { clientHeight } = cm.getScrollInfo();
cm.scrollIntoView({ line: found, ch: 0 }, clientHeight >>> 1);
};
const clearSearch = function(cm, hard) {
cm.operation(function() {
const state = getSearchState(cm);
if ( state.query ) {
state.query = state.queryText = null;
}
state.lines = [];
if ( state.overlay !== undefined ) {
cm.removeOverlay(state.overlay);
state.overlay = undefined;
}
if ( state.annotate ) {
state.annotate.clear();
state.annotate = undefined;
}
state.widget.removeAttribute('data-query');
if ( hard ) {
state.panel.clear();
state.panel = null;
state.widget = null;
cm.state.search = null;
}
});
};
const findCommit = function(cm, dir) {
const state = getSearchState(cm);
state.queryTimer.off();
const queryText = queryTextFromSearchWidget(cm);
if ( queryText === state.queryText ) { return; }
state.queryText = queryText;
if ( state.queryText === '' ) {
clearSearch(cm);
} else {
cm.operation(function() {
startSearch(cm, state);
findNext(cm, dir);
});
}
};
const findCommand = function(cm) {
let queryText = cm.getSelection() || undefined;
if ( !queryText ) {
const word = cm.findWordAt(cm.getCursor());
queryText = cm.getRange(word.anchor, word.head);
if ( /^\W|\W$/.test(queryText) ) {
queryText = undefined;
}
cm.setCursor(word.anchor);
}
queryTextToSearchWidget(cm, queryText);
findCommit(cm, 1);
};
const findNextCommand = function(cm) {
const state = getSearchState(cm);
if ( state.query ) { return findNext(cm, 1); }
};
const findPrevCommand = function(cm) {
const state = getSearchState(cm);
if ( state.query ) { return findNext(cm, -1); }
};
{
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);
document.body.appendChild(widgetTemplate);
}
CodeMirror.commands.find = findCommand;
CodeMirror.commands.findNext = findNextCommand;
CodeMirror.commands.findPrev = findPrevCommand;
CodeMirror.defineInitHook(function(cm) {
getSearchState(cm);
cm.on('linterDone', details => {
const linterWidget = qs$('.cm-linter-widget');
const count = details.errorCount;
if ( linterWidget.dataset.lint === `${count}` ) { return; }
linterWidget.dataset.lint = `${count}`;
dom.text(
qs$(linterWidget, '.cm-linter-widget-count'),
i18n$('linterMainReport').replace('{{count}}', count.toLocaleString())
);
});
});
}

View File

@@ -0,0 +1,239 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2019-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
*/
/* global CodeMirror */
'use strict';
CodeMirror.defineMode('ubo-dynamic-filtering', ( ) => {
const validSwitches = new Set([
'no-strict-blocking:',
'no-popups:',
'no-cosmetic-filtering:',
'no-remote-fonts:',
'no-large-media:',
'no-csp-reports:',
'no-scripting:',
]);
const validSwitcheStates = new Set([
'true',
'false',
]);
const validHnRuleTypes = new Set([
'*',
'3p',
'image',
'inline-script',
'1p-script',
'3p-script',
'3p-frame',
]);
const invalidURLRuleTypes = new Set([
'doc',
'main_frame',
]);
const validActions = new Set([
'block',
'allow',
'noop',
]);
const hnValidator = new URL(self.location.href);
const reBadHn = /[%]|^\.|\.$/;
const slices = [];
let sliceIndex = 0;
let sliceCount = 0;
let hostnameToDomainMap = new Map();
let psl;
const isValidHostname = hnin => {
if ( hnin === '*' ) { return true; }
hnValidator.hostname = '_';
try {
hnValidator.hostname = hnin;
} catch(_) {
return false;
}
const hnout = hnValidator.hostname;
return hnout !== '_' && hnout !== '' && reBadHn.test(hnout) === false;
};
const addSlice = (len, style = null) => {
let i = sliceCount;
if ( i === slices.length ) {
slices[i] = { len: 0, style: null };
}
const entry = slices[i];
entry.len = len;
entry.style = style;
sliceCount += 1;
};
const addMatchSlice = (match, style = null) => {
const len = match !== null ? match[0].length : 0;
addSlice(len, style);
return match !== null ? match.input.slice(len) : '';
};
const addMatchHnSlices = (match, style = null) => {
const hn = match[0];
if ( hn === '*' ) {
return addMatchSlice(match, style);
}
let dn = hostnameToDomainMap.get(hn) || '';
if ( dn === '' && psl !== undefined ) {
dn = /(\d|\])$/.test(hn) ? hn : (psl.getDomain(hn) || hn);
}
const entityBeg = hn.length - dn.length;
if ( entityBeg !== 0 ) {
addSlice(entityBeg, style);
}
let entityEnd = dn.indexOf('.');
if ( entityEnd === -1 ) { entityEnd = dn.length; }
addSlice(entityEnd, style !== null ? `${style} strong` : 'strong');
if ( entityEnd < dn.length ) {
addSlice(dn.length - entityEnd, style);
}
return match.input.slice(hn.length);
};
const makeSlices = (stream, opts) => {
sliceIndex = 0;
sliceCount = 0;
let { string } = stream;
if ( string === '...' ) { return; }
const { sortType } = opts;
const reNotToken = /^\s+/;
const reToken = /^\S+/;
const tokens = [];
// leading whitespaces
let match = reNotToken.exec(string);
if ( match !== null ) {
string = addMatchSlice(match);
}
// first token
match = reToken.exec(string);
if ( match === null ) { return; }
tokens.push(match[0]);
// hostname or switch
const isSwitchRule = validSwitches.has(match[0]);
if ( isSwitchRule ) {
string = addMatchSlice(match, sortType === 0 ? 'sortkey' : null);
} else if ( isValidHostname(match[0]) ) {
if ( sortType === 1 ) {
string = addMatchHnSlices(match, 'sortkey');
} else {
string = addMatchHnSlices(match, null);
}
} else {
string = addMatchSlice(match, 'error');
}
// whitespaces before second token
match = reNotToken.exec(string);
if ( match === null ) { return; }
string = addMatchSlice(match);
// second token
match = reToken.exec(string);
if ( match === null ) { return; }
tokens.push(match[0]);
// hostname or url
const isURLRule = isSwitchRule === false && match[0].indexOf('://') > 0;
if ( isURLRule ) {
string = addMatchSlice(match, sortType === 2 ? 'sortkey' : null);
} else if ( isValidHostname(match[0]) === false ) {
string = addMatchSlice(match, 'error');
} else if ( sortType === 1 && isSwitchRule || sortType === 2 ) {
string = addMatchHnSlices(match, 'sortkey');
} else {
string = addMatchHnSlices(match, null);
}
// whitespaces before third token
match = reNotToken.exec(string);
if ( match === null ) { return; }
string = addMatchSlice(match);
// third token
match = reToken.exec(string);
if ( match === null ) { return; }
tokens.push(match[0]);
// rule type or switch state
if ( isSwitchRule ) {
string = validSwitcheStates.has(match[0])
? addMatchSlice(match, match[0] === 'true' ? 'blockrule' : 'allowrule')
: addMatchSlice(match, 'error');
} else if ( isURLRule ) {
string = invalidURLRuleTypes.has(match[0])
? addMatchSlice(match, 'error')
: addMatchSlice(match);
} else if ( tokens[1] === '*' ) {
string = validHnRuleTypes.has(match[0])
? addMatchSlice(match)
: addMatchSlice(match, 'error');
} else {
string = match[0] === '*'
? addMatchSlice(match)
: addMatchSlice(match, 'error');
}
// whitespaces before fourth token
match = reNotToken.exec(string);
if ( match === null ) { return; }
string = addMatchSlice(match);
// fourth token
match = reToken.exec(string);
if ( match === null ) { return; }
tokens.push(match[0]);
string = isSwitchRule || validActions.has(match[0]) === false
? addMatchSlice(match, 'error')
: addMatchSlice(match, `${match[0]}rule`);
// whitespaces before end of line
match = reNotToken.exec(string);
if ( match === null ) { return; }
string = addMatchSlice(match);
// any token beyond fourth token is invalid
match = reToken.exec(string);
if ( match !== null ) {
string = addMatchSlice(null, 'error');
}
};
const token = function(stream) {
if ( stream.sol() ) {
makeSlices(stream, this);
}
if ( sliceIndex >= sliceCount ) {
stream.skipToEnd(stream);
return null;
}
const { len, style } = slices[sliceIndex++];
if ( len === 0 ) {
stream.skipToEnd();
} else {
stream.pos += len;
}
return style;
};
return {
token,
sortType: 1,
setHostnameToDomainMap: a => { hostnameToDomainMap = a; },
setPSL: a => { psl = a; },
};
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,191 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2017-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';
/******************************************************************************/
import µb from './background.js';
import { hostnameFromURI } from './uri-utils.js';
/******************************************************************************/
(( ) => {
// *****************************************************************************
// start of local namespace
if ( vAPI.commands instanceof Object === false ) { return; }
const relaxBlockingMode = (( ) => {
const reloadTimers = new Map();
return function(tab) {
if ( tab instanceof Object === false || tab.id <= 0 ) { return; }
const normalURL = µb.normalizeTabURL(tab.id, tab.url);
if ( µb.getNetFilteringSwitch(normalURL) === false ) { return; }
const hn = hostnameFromURI(normalURL);
const curProfileBits = µb.blockingModeFromHostname(hn);
let newProfileBits;
for ( const profile of µb.liveBlockingProfiles ) {
if ( (curProfileBits & profile.bits & ~1) !== curProfileBits ) {
newProfileBits = profile.bits;
break;
}
}
// TODO: Reset to original blocking profile?
if ( newProfileBits === undefined ) { return; }
const noReload = (newProfileBits & 0b00000001) === 0;
if (
(curProfileBits & 0b00000010) !== 0 &&
(newProfileBits & 0b00000010) === 0
) {
µb.toggleHostnameSwitch({
name: 'no-scripting',
hostname: hn,
state: false,
});
}
if ( µb.userSettings.advancedUserEnabled ) {
if (
(curProfileBits & 0b00000100) !== 0 &&
(newProfileBits & 0b00000100) === 0
) {
µb.toggleFirewallRule({
tabId: noReload ? tab.id : undefined,
srcHostname: hn,
desHostname: '*',
requestType: '3p',
action: 3,
});
}
if (
(curProfileBits & 0b00001000) !== 0 &&
(newProfileBits & 0b00001000) === 0
) {
µb.toggleFirewallRule({
srcHostname: hn,
desHostname: '*',
requestType: '3p-script',
action: 3,
});
}
if (
(curProfileBits & 0b00010000) !== 0 &&
(newProfileBits & 0b00010000) === 0
) {
µb.toggleFirewallRule({
srcHostname: hn,
desHostname: '*',
requestType: '3p-frame',
action: 3,
});
}
}
// Reload the target tab?
if ( noReload ) { return; }
// Reload: use a timer to coalesce bursts of reload commands.
const timer = reloadTimers.get(tab.id) || (( ) => {
const t = vAPI.defer.create(tabId => {
reloadTimers.delete(tabId);
vAPI.tabs.reload(tabId);
});
reloadTimers.set(tab.id, t);
return t;
})();
timer.offon(547, tab.id);
};
})();
vAPI.commands.onCommand.addListener(async command => {
// Generic commands
if ( command === 'open-dashboard' ) {
µb.openNewTab({
url: 'dashboard.html',
select: true,
index: -1,
});
return;
}
// 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(
tab.id,
0,
undefined,
command === 'launch-element-zapper'
);
break;
}
case 'launch-logger': {
const hash = tab.url.startsWith(vAPI.getURL(''))
? ''
: `#_+${tab.id}`;
µb.openNewTab({
url: `logger-ui.html${hash}`,
select: true,
index: -1,
});
break;
}
case 'relax-blocking-mode':
relaxBlockingMode(tab);
break;
case 'toggle-cosmetic-filtering':
µb.toggleHostnameSwitch({
name: 'no-cosmetic-filtering',
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;
}
});
// end of local namespace
// *****************************************************************************
})();
/******************************************************************************/

View File

@@ -0,0 +1,59 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2019-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';
/******************************************************************************/
function ubologSet(state = false) {
if ( state ) {
if ( ubolog.process instanceof Function ) {
ubolog.process();
}
ubolog = ubologDo;
} else {
ubolog = ubologIgnore;
}
}
function ubologDo(...args) {
console.info('[uBO]', ...args);
}
function ubologIgnore() {
}
let ubolog = (( ) => {
const pending = [];
const store = function(...args) {
pending.push(args);
};
store.process = function() {
for ( const args of pending ) {
ubologDo(...args);
}
};
return store;
})();
/******************************************************************************/
export { ubolog, ubologSet };

View File

@@ -0,0 +1,717 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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
*/
if (
typeof vAPI === 'object' &&
typeof vAPI.DOMProceduralFilterer !== 'object'
) {
// >>>>>>>> start of local scope
/******************************************************************************/
const nonVisualElements = {
head: true,
link: true,
meta: true,
script: true,
style: true,
};
const regexFromString = (s, exact = false) => {
if ( s === '' ) { return /^/; }
const match = /^\/(.+)\/([imu]*)$/.exec(s);
if ( match !== null ) {
return new RegExp(match[1], match[2] || undefined);
}
const reStr = s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return new RegExp(exact ? `^${reStr}$` : reStr);
};
// 'P' stands for 'Procedural'
class PSelectorTask {
begin() {
}
end() {
}
}
class PSelectorVoidTask extends PSelectorTask {
constructor(task) {
super();
console.info(`uBO: :${task[0]}() operator does not exist`);
}
transpose() {
}
}
class PSelectorHasTextTask extends PSelectorTask {
constructor(task) {
super();
this.needle = regexFromString(task[1]);
}
transpose(node, output) {
if ( this.needle.test(node.textContent) ) {
output.push(node);
}
}
}
class PSelectorIfTask extends PSelectorTask {
constructor(task) {
super();
this.pselector = new PSelector(task[1]);
}
transpose(node, output) {
if ( this.pselector.test(node) === this.target ) {
output.push(node);
}
}
}
PSelectorIfTask.prototype.target = true;
class PSelectorIfNotTask extends PSelectorIfTask {
}
PSelectorIfNotTask.prototype.target = false;
class PSelectorMatchesAttrTask extends PSelectorTask {
constructor(task) {
super();
this.reAttr = regexFromString(task[1].attr, true);
this.reValue = regexFromString(task[1].value, true);
}
transpose(node, output) {
const attrs = node.getAttributeNames();
for ( const attr of attrs ) {
if ( this.reAttr.test(attr) === false ) { continue; }
if ( this.reValue.test(node.getAttribute(attr)) === false ) { continue; }
output.push(node);
}
}
}
class PSelectorMatchesCSSTask extends PSelectorTask {
constructor(task) {
super();
this.name = task[1].name;
this.pseudo = task[1].pseudo ? `::${task[1].pseudo}` : null;
let arg0 = task[1].value, arg1;
if ( Array.isArray(arg0) ) {
arg1 = arg0[1]; arg0 = arg0[0];
}
this.value = new RegExp(arg0, arg1);
}
transpose(node, output) {
const style = window.getComputedStyle(node, this.pseudo);
if ( style !== null && this.value.test(style[this.name]) ) {
output.push(node);
}
}
}
class PSelectorMatchesCSSAfterTask extends PSelectorMatchesCSSTask {
constructor(task) {
super(task);
this.pseudo = '::after';
}
}
class PSelectorMatchesCSSBeforeTask extends PSelectorMatchesCSSTask {
constructor(task) {
super(task);
this.pseudo = '::before';
}
}
class PSelectorMatchesMediaTask extends PSelectorTask {
constructor(task) {
super();
this.mql = window.matchMedia(task[1]);
if ( this.mql.media === 'not all' ) { return; }
this.mql.addEventListener('change', ( ) => {
if ( typeof vAPI !== 'object' ) { return; }
if ( vAPI === null ) { return; }
const filterer = vAPI.domFilterer && vAPI.domFilterer.proceduralFilterer;
if ( filterer instanceof Object === false ) { return; }
filterer.onDOMChanged([ null ]);
});
}
transpose(node, output) {
if ( this.mql.matches === false ) { return; }
output.push(node);
}
}
class PSelectorMatchesPathTask extends PSelectorTask {
constructor(task) {
super();
this.needle = regexFromString(
task[1].replace(/\P{ASCII}/gu, s => encodeURIComponent(s))
);
}
transpose(node, output) {
if ( this.needle.test(self.location.pathname + self.location.search) ) {
output.push(node);
}
}
}
class PSelectorMatchesPropTask extends PSelectorTask {
constructor(task) {
super();
this.props = task[1].attr.split('.');
this.reValue = task[1].value !== ''
? regexFromString(task[1].value, true)
: null;
}
transpose(node, output) {
let value = node;
for ( const prop of this.props ) {
if ( value === undefined ) { return; }
if ( value === null ) { return; }
value = value[prop];
}
if ( this.reValue === null ) {
if ( value === undefined ) { return; }
} else if ( this.reValue.test(value) === false ) {
return;
}
output.push(node);
}
}
class PSelectorMinTextLengthTask extends PSelectorTask {
constructor(task) {
super();
this.min = task[1];
}
transpose(node, output) {
if ( node.textContent.length >= this.min ) {
output.push(node);
}
}
}
class PSelectorOthersTask extends PSelectorTask {
constructor() {
super();
this.targets = new Set();
}
begin() {
this.targets.clear();
}
end(output) {
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 && keep !== head ) {
toKeep.add(keep);
toDiscard.delete(keep);
discard = keep.previousElementSibling;
while ( discard !== null ) {
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 ) {
if ( toKeep.has(discard) === false ) {
toDiscard.add(discard);
}
}
discard = discard.nextElementSibling;
}
keep = keep.parentElement;
}
}
for ( discard of toDiscard ) {
output.push(discard);
}
this.targets.clear();
}
transpose(candidate) {
for ( const target of this.targets ) {
if ( target.contains(candidate) ) { return; }
if ( candidate.contains(target) ) {
this.targets.delete(target);
}
}
this.targets.add(candidate);
}
}
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 {
constructor(task) {
super();
this.spath = task[1];
this.nth = /^(?:\s*[+~]|:)/.test(this.spath);
if ( this.nth ) { return; }
if ( /^\s*>/.test(this.spath) ) {
this.spath = `:scope ${this.spath.trim()}`;
}
}
transpose(node, output) {
const nodes = this.nth
? PSelectorSpathTask.qsa(node, this.spath)
: node.querySelectorAll(this.spath);
for ( const node of nodes ) {
output.push(node);
}
}
// Helper method for other operators.
static qsa(node, selector) {
const parent = node.parentElement;
if ( parent === null ) { return []; }
let pos = 1;
for (;;) {
node = node.previousElementSibling;
if ( node === null ) { break; }
pos += 1;
}
return parent.querySelectorAll(
`:scope > :nth-child(${pos})${selector}`
);
}
}
class PSelectorUpwardTask extends PSelectorTask {
constructor(task) {
super();
const arg = task[1];
if ( typeof arg === 'number' ) {
this.i = arg;
} else {
this.s = arg;
}
}
transpose(node, output) {
if ( this.s !== '' ) {
const parent = node.parentElement;
if ( parent === null ) { return; }
node = parent.closest(this.s);
if ( node === null ) { return; }
} else {
let nth = this.i;
for (;;) {
node = node.parentElement;
if ( node === null ) { return; }
nth -= 1;
if ( nth === 0 ) { break; }
}
}
output.push(node);
}
}
PSelectorUpwardTask.prototype.i = 0;
PSelectorUpwardTask.prototype.s = '';
class PSelectorWatchAttrs extends PSelectorTask {
constructor(task) {
super();
this.observer = null;
this.observed = new WeakSet();
this.observerOptions = {
attributes: true,
subtree: true,
};
const attrs = task[1];
if ( Array.isArray(attrs) && attrs.length !== 0 ) {
this.observerOptions.attributeFilter = task[1];
}
}
// TODO: Is it worth trying to re-apply only the current selector?
handler() {
const filterer =
vAPI.domFilterer && vAPI.domFilterer.proceduralFilterer;
if ( filterer instanceof Object ) {
filterer.onDOMChanged([ null ]);
}
}
transpose(node, output) {
output.push(node);
if ( this.observed.has(node) ) { return; }
if ( this.observer === null ) {
this.observer = new MutationObserver(this.handler);
}
this.observer.observe(node, this.observerOptions);
this.observed.add(node);
}
}
class PSelectorXpathTask extends PSelectorTask {
constructor(task) {
super();
this.xpe = document.createExpression(task[1], null);
this.xpr = null;
}
transpose(node, output) {
this.xpr = this.xpe.evaluate(
node,
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
this.xpr
);
let j = this.xpr.snapshotLength;
while ( j-- ) {
const node = this.xpr.snapshotItem(j);
if ( node.nodeType === 1 ) {
output.push(node);
}
}
}
}
class PSelector {
constructor(o) {
this.selector = o.selector;
this.tasks = [];
const tasks = [];
if ( Array.isArray(o.tasks) === false ) { return; }
for ( const task of o.tasks ) {
const ctor = this.operatorToTaskMap.get(task[0]) || PSelectorVoidTask;
tasks.push(new ctor(task));
}
this.tasks = tasks;
}
prime(input) {
const root = input || document;
if ( this.selector === '' ) { return [ root ]; }
if ( input !== document ) {
const c0 = this.selector.charCodeAt(0);
if ( c0 === 0x2B /* + */ || c0 === 0x7E /* ~ */ ) {
return Array.from(PSelectorSpathTask.qsa(input, this.selector));
} else if ( c0 === 0x3E /* > */ ) {
return Array.from(input.querySelectorAll(`:scope ${this.selector}`));
}
}
return Array.from(root.querySelectorAll(this.selector));
}
exec(input) {
let nodes = this.prime(input);
for ( const task of this.tasks ) {
if ( nodes.length === 0 ) { break; }
const transposed = [];
task.begin();
for ( const node of nodes ) {
task.transpose(node, transposed);
}
task.end(transposed);
nodes = transposed;
}
return nodes;
}
test(input) {
const nodes = this.prime(input);
for ( const node of nodes ) {
let output = [ node ];
for ( const task of this.tasks ) {
const transposed = [];
task.begin();
for ( const node of output ) {
task.transpose(node, transposed);
}
task.end(transposed);
output = transposed;
if ( output.length === 0 ) { break; }
}
if ( output.length !== 0 ) { return true; }
}
return false;
}
}
PSelector.prototype.operatorToTaskMap = new Map([
[ 'has', PSelectorIfTask ],
[ 'has-text', PSelectorHasTextTask ],
[ 'if', PSelectorIfTask ],
[ 'if-not', PSelectorIfNotTask ],
[ 'matches-attr', PSelectorMatchesAttrTask ],
[ 'matches-css', PSelectorMatchesCSSTask ],
[ 'matches-css-after', PSelectorMatchesCSSAfterTask ],
[ 'matches-css-before', PSelectorMatchesCSSBeforeTask ],
[ 'matches-media', PSelectorMatchesMediaTask ],
[ 'matches-path', PSelectorMatchesPathTask ],
[ 'matches-prop', PSelectorMatchesPropTask ],
[ 'min-text-length', PSelectorMinTextLengthTask ],
[ 'not', PSelectorIfNotTask ],
[ 'others', PSelectorOthersTask ],
[ 'shadow', PSelectorShadowTask ],
[ 'spath', PSelectorSpathTask ],
[ 'upward', PSelectorUpwardTask ],
[ 'watch-attr', PSelectorWatchAttrs ],
[ 'xpath', PSelectorXpathTask ],
]);
class PSelectorRoot extends PSelector {
constructor(o) {
super(o);
this.budget = 200; // I arbitrary picked a 1/5 second
this.raw = o.raw;
this.cost = 0;
this.lastAllowanceTime = 0;
this.action = o.action;
}
prime(input) {
try {
return super.prime(input);
} catch (ex) {
}
return [];
}
}
PSelectorRoot.prototype.hit = false;
class ProceduralFilterer {
constructor(domFilterer) {
this.domFilterer = domFilterer;
this.mustApplySelectors = false;
this.selectors = new Map();
this.masterToken = vAPI.randomToken();
this.styleTokenMap = new Map();
this.styledNodes = new Set();
if ( vAPI.domWatcher instanceof Object ) {
vAPI.domWatcher.addListener(this);
}
}
addProceduralSelectors(selectors) {
const addedSelectors = [];
let mustCommit = false;
for ( const selector of selectors ) {
if ( this.selectors.has(selector.raw) ) { continue; }
const pselector = new PSelectorRoot(selector);
this.primeProceduralSelector(pselector);
this.selectors.set(selector.raw, pselector);
addedSelectors.push(pselector);
mustCommit = true;
}
if ( mustCommit === false ) { return; }
this.mustApplySelectors = this.selectors.size !== 0;
this.domFilterer.commit();
if ( this.domFilterer.hasListeners() ) {
this.domFilterer.triggerListeners({
procedural: addedSelectors
});
}
}
// This allows to perform potentially expensive initialization steps
// before the filters are ready to be applied.
primeProceduralSelector(pselector) {
if ( pselector.action === undefined ) {
this.styleTokenFromStyle(vAPI.hideStyle);
} else if ( pselector.action[0] === 'style' ) {
this.styleTokenFromStyle(pselector.action[1]);
}
return pselector;
}
commitNow() {
if ( this.selectors.size === 0 ) { return; }
this.mustApplySelectors = false;
// https://github.com/uBlockOrigin/uBlock-issues/issues/341
// Be ready to unhide nodes which no longer matches any of
// the procedural selectors.
const toUnstyle = this.styledNodes;
this.styledNodes = new Set();
let t0 = Date.now();
for ( const pselector of this.selectors.values() ) {
const allowance = Math.floor((t0 - pselector.lastAllowanceTime) / 2000);
if ( allowance >= 1 ) {
pselector.budget += allowance * 50;
if ( pselector.budget > 200 ) { pselector.budget = 200; }
pselector.lastAllowanceTime = t0;
}
if ( pselector.budget <= 0 ) { continue; }
const nodes = pselector.exec();
const t1 = Date.now();
pselector.budget += t0 - t1;
if ( pselector.budget < -500 ) {
console.info('uBO: disabling %s', pselector.raw);
pselector.budget = -0x7FFFFFFF;
}
t0 = t1;
if ( nodes.length === 0 ) { continue; }
pselector.hit = true;
this.processNodes(nodes, pselector.action);
}
this.unprocessNodes(toUnstyle);
}
styleTokenFromStyle(style) {
if ( style === undefined ) { return; }
let styleToken = this.styleTokenMap.get(style);
if ( styleToken !== undefined ) { return styleToken; }
styleToken = vAPI.randomToken();
this.styleTokenMap.set(style, styleToken);
this.domFilterer.addCSS(
`[${this.masterToken}][${styleToken}]\n{${style}}`,
{ silent: true, mustInject: true }
);
return styleToken;
}
processNodes(nodes, action) {
const op = action && action[0] || '';
const arg = op !== '' ? action[1] : '';
switch ( op ) {
case '':
/* fall through */
case 'style': {
const styleToken = this.styleTokenFromStyle(
arg === '' ? vAPI.hideStyle : arg
);
for ( const node of nodes ) {
node.setAttribute(this.masterToken, '');
node.setAttribute(styleToken, '');
this.styledNodes.add(node);
}
break;
}
case 'remove': {
for ( const node of nodes ) {
node.remove();
node.textContent = '';
}
break;
}
case 'remove-attr': {
const reAttr = regexFromString(arg, true);
for ( const node of nodes ) {
for ( const name of node.getAttributeNames() ) {
if ( reAttr.test(name) === false ) { continue; }
node.removeAttribute(name);
}
}
break;
}
case 'remove-class': {
const reClass = regexFromString(arg, true);
for ( const node of nodes ) {
const cl = node.classList;
for ( const name of cl.values() ) {
if ( reClass.test(name) === false ) { continue; }
cl.remove(name);
}
}
break;
}
default:
break;
}
}
// TODO: Current assumption is one style per hit element. Could be an
// issue if an element has multiple styling and one styling is
// brought back. Possibly too rare to care about this for now.
unprocessNodes(nodes) {
for ( const node of nodes ) {
if ( this.styledNodes.has(node) ) { continue; }
node.removeAttribute(this.masterToken);
}
}
createProceduralFilter(o) {
return this.primeProceduralSelector(
new PSelectorRoot(typeof o === 'string' ? JSON.parse(o) : o)
);
}
onDOMCreated() {
}
onDOMChanged(addedNodes, removedNodes) {
if ( this.selectors.size === 0 ) { return; }
this.mustApplySelectors =
this.mustApplySelectors ||
addedNodes.length !== 0 ||
removedNodes;
this.domFilterer.commit();
}
}
vAPI.DOMProceduralFilterer = ProceduralFilterer;
/******************************************************************************/
// >>>>>>>> end of local scope
}
/*******************************************************************************
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;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,273 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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
*/
import { i18n$ } from './i18n.js';
import µb from './background.js';
/******************************************************************************/
const contextMenu = (( ) => {
/******************************************************************************/
if ( vAPI.contextMenu === undefined ) {
return {
update: function() {}
};
}
/******************************************************************************/
const BLOCK_ELEMENT_BIT = 0b00001;
const BLOCK_RESOURCE_BIT = 0b00010;
const TEMP_ALLOW_LARGE_MEDIA_BIT = 0b00100;
const SUBSCRIBE_TO_LIST_BIT = 0b01000;
const VIEW_SOURCE_BIT = 0b10000;
/******************************************************************************/
const onBlockElement = function(details, tab) {
if ( tab === undefined ) { return; }
if ( /^https?:\/\//.test(tab.url) === false ) { return; }
let tagName = details.tagName || '';
let src = details.frameUrl || details.srcUrl || details.linkUrl || '';
if ( !tagName ) {
if ( typeof details.frameUrl === 'string' && details.frameId !== 0 ) {
tagName = 'iframe';
src = details.srcUrl;
} else if ( typeof details.srcUrl === 'string' ) {
if ( details.mediaType === 'image' ) {
tagName = 'img';
src = details.srcUrl;
} else if ( details.mediaType === 'video' ) {
tagName = 'video';
src = details.srcUrl;
} else if ( details.mediaType === 'audio' ) {
tagName = 'audio';
src = details.srcUrl;
}
} else if ( typeof details.linkUrl === 'string' ) {
tagName = 'a';
src = details.linkUrl;
}
}
µb.epickerArgs.mouse = true;
µb.elementPickerExec(tab.id, 0, `${tagName}\t${src}`);
};
/******************************************************************************/
const onBlockElementInFrame = function(details, tab) {
if ( tab === undefined ) { return; }
if ( /^https?:\/\//.test(details.frameUrl) === false ) { return; }
µb.epickerArgs.mouse = false;
µb.elementPickerExec(tab.id, details.frameId);
};
/******************************************************************************/
const onSubscribeToList = function(details) {
let parsedURL;
try {
parsedURL = new URL(details.linkUrl);
}
catch(ex) {
}
if ( parsedURL instanceof URL === false ) { return; }
const url = parsedURL.searchParams.get('location');
if ( url === null ) { return; }
const title = parsedURL.searchParams.get('title') || '?';
const hash = µb.selectedFilterLists.indexOf(parsedURL) !== -1
? '#subscribed'
: '';
vAPI.tabs.open({
url:
`/asset-viewer.html` +
`?url=${encodeURIComponent(url)}` +
`&title=${encodeURIComponent(title)}` +
`&subscribe=1${hash}`,
select: true,
});
};
/******************************************************************************/
const onTemporarilyAllowLargeMediaElements = function(details, tab) {
if ( tab === undefined ) { return; }
const pageStore = µb.pageStoreFromTabId(tab.id);
if ( pageStore === null ) { return; }
pageStore.temporarilyAllowLargeMediaElements(true);
};
/******************************************************************************/
const onViewSource = function(details, tab) {
if ( tab === undefined ) { return; }
const url = details.linkUrl || details.frameUrl || details.pageUrl || '';
if ( /^https?:\/\//.test(url) === false ) { return; }
µb.openNewTab({
url: `code-viewer.html?url=${self.encodeURIComponent(url)}`,
select: true,
});
};
/******************************************************************************/
const onEntryClicked = function(details, tab) {
if ( details.menuItemId === 'uBlock0-blockElement' ) {
return onBlockElement(details, tab);
}
if ( details.menuItemId === 'uBlock0-blockElementInFrame' ) {
return onBlockElementInFrame(details, tab);
}
if ( details.menuItemId === 'uBlock0-blockResource' ) {
return onBlockElement(details, tab);
}
if ( details.menuItemId === 'uBlock0-subscribeToList' ) {
return onSubscribeToList(details);
}
if ( details.menuItemId === 'uBlock0-temporarilyAllowLargeMediaElements' ) {
return onTemporarilyAllowLargeMediaElements(details, tab);
}
if ( details.menuItemId === 'uBlock0-viewSource' ) {
return onViewSource(details, tab);
}
};
/******************************************************************************/
const menuEntries = {
blockElement: {
id: 'uBlock0-blockElement',
title: i18n$('pickerContextMenuEntry'),
contexts: [ 'all' ],
documentUrlPatterns: [ 'http://*/*', 'https://*/*' ],
},
blockElementInFrame: {
id: 'uBlock0-blockElementInFrame',
title: i18n$('contextMenuBlockElementInFrame'),
contexts: [ 'frame' ],
documentUrlPatterns: [ 'http://*/*', 'https://*/*' ],
},
blockResource: {
id: 'uBlock0-blockResource',
title: i18n$('pickerContextMenuEntry'),
contexts: [ 'audio', 'frame', 'image', 'video' ],
documentUrlPatterns: [ 'http://*/*', 'https://*/*' ],
},
subscribeToList: {
id: 'uBlock0-subscribeToList',
title: i18n$('contextMenuSubscribeToList'),
contexts: [ 'link' ],
targetUrlPatterns: [ 'abp:*', 'https://subscribe.adblockplus.org/*' ],
},
temporarilyAllowLargeMediaElements: {
id: 'uBlock0-temporarilyAllowLargeMediaElements',
title: i18n$('contextMenuTemporarilyAllowLargeMediaElements'),
contexts: [ 'all' ],
documentUrlPatterns: [ 'http://*/*', 'https://*/*' ],
},
viewSource: {
id: 'uBlock0-viewSource',
title: i18n$('contextMenuViewSource'),
contexts: [ 'page', 'frame', 'link' ],
documentUrlPatterns: [ 'http://*/*', 'https://*/*' ],
},
};
/******************************************************************************/
let currentBits = 0;
const update = function(tabId = undefined) {
let newBits = 0;
if ( µb.userSettings.contextMenuEnabled ) {
const pageStore = tabId && µb.pageStoreFromTabId(tabId) || null;
if ( pageStore?.getNetFilteringSwitch() ) {
if ( µb.userFiltersAreEnabled() ) {
if ( pageStore.shouldApplySpecificCosmeticFilters(0) ) {
newBits |= BLOCK_ELEMENT_BIT;
} else {
newBits |= BLOCK_RESOURCE_BIT;
}
}
if ( pageStore.largeMediaCount !== 0 ) {
newBits |= TEMP_ALLOW_LARGE_MEDIA_BIT;
}
}
if ( µb.hiddenSettings.filterAuthorMode ) {
newBits |= VIEW_SOURCE_BIT;
}
}
newBits |= SUBSCRIBE_TO_LIST_BIT;
if ( newBits === currentBits ) { return; }
currentBits = newBits;
const usedEntries = [];
if ( (newBits & BLOCK_ELEMENT_BIT) !== 0 ) {
usedEntries.push(menuEntries.blockElement);
usedEntries.push(menuEntries.blockElementInFrame);
}
if ( (newBits & BLOCK_RESOURCE_BIT) !== 0 ) {
usedEntries.push(menuEntries.blockResource);
}
if ( (newBits & TEMP_ALLOW_LARGE_MEDIA_BIT) !== 0 ) {
usedEntries.push(menuEntries.temporarilyAllowLargeMediaElements);
}
if ( (newBits & SUBSCRIBE_TO_LIST_BIT) !== 0 ) {
usedEntries.push(menuEntries.subscribeToList);
}
if ( (newBits & VIEW_SOURCE_BIT) !== 0 ) {
usedEntries.push(menuEntries.viewSource);
}
vAPI.contextMenu.setEntries(usedEntries, onEntryClicked);
};
/******************************************************************************/
// https://github.com/uBlockOrigin/uBlock-issues/issues/151
// For unknown reasons, the currently active tab will not be successfully
// looked up after closing a window.
vAPI.contextMenu.onMustUpdate = async function(tabId = undefined) {
if ( µb.userSettings.contextMenuEnabled === false ) {
return update();
}
if ( tabId !== undefined ) {
return update(tabId);
}
const tab = await vAPI.tabs.getCurrent();
if ( tab instanceof Object === false ) { return; }
update(tab.id);
};
return { update: vAPI.contextMenu.onMustUpdate };
/******************************************************************************/
})();
/******************************************************************************/
export default contextMenu;
/******************************************************************************/

View File

@@ -0,0 +1,986 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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
*/
/******************************************************************************/
import { MRUCache } from './mrucache.js';
import { StaticExtFilteringHostnameDB } from './static-ext-filtering-db.js';
import logger from './logger.js';
import µb from './background.js';
/******************************************************************************/
/******************************************************************************/
const SelectorCacheEntry = class {
constructor() {
this.reset();
}
reset() {
this.cosmetic = new Set();
this.cosmeticHashes = new Set();
this.disableSurveyor = false;
this.net = new Map();
this.accessId = SelectorCacheEntry.accessId++;
return this;
}
dispose() {
this.cosmetic = this.cosmeticHashes = this.net = null;
if ( SelectorCacheEntry.junkyard.length < 25 ) {
SelectorCacheEntry.junkyard.push(this);
}
}
addCosmetic(details) {
const selectors = details.selectors.join(',\n');
if ( selectors.length !== 0 ) {
this.cosmetic.add(selectors);
}
for ( const hash of details.hashes ) {
this.cosmeticHashes.add(hash);
}
}
addNet(selectors) {
if ( typeof selectors === 'string' ) {
this.net.set(selectors, this.accessId);
} else {
this.net.set(selectors.join(',\n'), this.accessId);
}
// Net request-derived selectors: I limit the number of cached
// selectors, as I expect cases where the blocked network requests
// are never the exact same URL.
if ( this.net.size < SelectorCacheEntry.netHighWaterMark ) { return; }
const keys = Array.from(this.net)
.sort((a, b) => b[1] - a[1])
.slice(SelectorCacheEntry.netLowWaterMark)
.map(a => a[0]);
for ( const key of keys ) {
this.net.delete(key);
}
}
addNetOne(selector, token) {
this.net.set(selector, token);
}
add(details) {
this.accessId = SelectorCacheEntry.accessId++;
if ( details.type === 'cosmetic' ) {
this.addCosmetic(details);
} else {
this.addNet(details.selectors);
}
}
// https://github.com/chrisaljoudi/uBlock/issues/420
remove(type) {
this.accessId = SelectorCacheEntry.accessId++;
if ( type === undefined || type === 'cosmetic' ) {
this.cosmetic.clear();
}
if ( type === undefined || type === 'net' ) {
this.net.clear();
}
}
retrieveToArray(iterator, out) {
for ( const selector of iterator ) {
out.push(selector);
}
}
retrieveToSet(iterator, out) {
for ( const selector of iterator ) {
out.add(selector);
}
}
retrieveNet(out) {
this.accessId = SelectorCacheEntry.accessId++;
if ( this.net.size === 0 ) { return false; }
this.retrieveToArray(this.net.keys(), out);
return true;
}
retrieveCosmetic(selectors, hashes) {
this.accessId = SelectorCacheEntry.accessId++;
if ( this.cosmetic.size === 0 ) { return false; }
this.retrieveToSet(this.cosmetic, selectors);
this.retrieveToArray(this.cosmeticHashes, hashes);
return true;
}
static factory() {
const entry = SelectorCacheEntry.junkyard.pop();
return entry
? entry.reset()
: new SelectorCacheEntry();
}
};
SelectorCacheEntry.accessId = 1;
SelectorCacheEntry.netLowWaterMark = 20;
SelectorCacheEntry.netHighWaterMark = 30;
SelectorCacheEntry.junkyard = [];
/******************************************************************************/
/******************************************************************************/
// http://www.cse.yorku.ca/~oz/hash.html#djb2
// Must mirror content script surveyor's version
const hashFromStr = (type, s) => {
const len = s.length;
const step = len + 7 >>> 3;
let hash = (type << 5) + type ^ len;
for ( let i = 0; i < len; i += step ) {
hash = (hash << 5) + hash ^ s.charCodeAt(i);
}
return hash & 0xFFFFFF;
};
// https://github.com/gorhill/uBlock/issues/1668
// The key must be literal: unescape escaped CSS before extracting key.
// It's an uncommon case, so it's best to unescape only when needed.
const keyFromSelector = selector => {
let matches = reSimplestSelector.exec(selector);
if ( matches !== null ) { return matches[0]; }
let key = '';
matches = rePlainSelector.exec(selector);
if ( matches !== null ) {
key = matches[0];
} else {
matches = rePlainSelectorEx.exec(selector);
if ( matches === null ) { return; }
key = matches[1] || matches[2];
}
if ( selector.includes(',') ) { return; }
if ( key.includes('\\') === false ) { return key; }
matches = rePlainSelectorEscaped.exec(selector);
if ( matches === null ) { return; }
key = '';
const escaped = matches[0];
let beg = 0;
reEscapeSequence.lastIndex = 0;
for (;;) {
matches = reEscapeSequence.exec(escaped);
if ( matches === null ) {
return key + escaped.slice(beg);
}
key += escaped.slice(beg, matches.index);
beg = reEscapeSequence.lastIndex;
if ( matches[1].length === 1 ) {
key += matches[1];
} else {
key += String.fromCharCode(parseInt(matches[1], 16));
}
}
};
const reSimplestSelector = /^[#.][\w-]+$/;
const rePlainSelector = /^[#.][\w\\-]+/;
const rePlainSelectorEx = /^[^#.[(]+([#.][\w-]+)|([#.][\w-]+)$/;
const rePlainSelectorEscaped = /^[#.](?:\\[0-9A-Fa-f]+ |\\.|\w|-)+/;
const reEscapeSequence = /\\([0-9A-Fa-f]+ |.)/g;
/******************************************************************************/
/******************************************************************************/
// Cosmetic filter family tree:
//
// Generic
// Low generic simple: class or id only
// Low generic complex: class or id + extra stuff after
// High generic:
// High-low generic: [alt="..."],[title="..."]
// High-medium generic: [href^="..."]
// High-high generic: everything else
// Specific
// Specific hostname
// Specific entity
// Generic filters can only be enforced once the main document is loaded.
// Specific filers can be enforced before the main document is loaded.
const CosmeticFilteringEngine = function() {
this.reSimpleHighGeneric = /^(?:[a-z]*\[[^\]]+\]|\S+)$/;
this.selectorCache = new Map();
this.selectorCachePruneDelay = 10; // 10 minutes
this.selectorCacheCountMin = 40;
this.selectorCacheCountMax = 50;
this.selectorCacheTimer = vAPI.defer.create(( ) => {
this.pruneSelectorCacheAsync();
});
// specific filters
this.specificFilters = new StaticExtFilteringHostnameDB(2);
// low generic cosmetic filters: map of hash => stringified selector list
this.lowlyGeneric = new Map();
// highly generic selectors sets
this.highlyGeneric = Object.create(null);
this.highlyGeneric.simple = {
canonical: 'highGenericHideSimple',
dict: new Set(),
str: '',
mru: new MRUCache(16)
};
this.highlyGeneric.complex = {
canonical: 'highGenericHideComplex',
dict: new Set(),
str: '',
mru: new MRUCache(16)
};
// Short-lived: content is valid only during one function call. These
// is to prevent repeated allocation/deallocation overheads -- the
// constructors/destructors of javascript Set/Map is assumed to be costlier
// than just calling clear() on these.
this.$specificSet = new Set();
this.$exceptionSet = new Set();
this.$proceduralSet = new Set();
this.$dummySet = new Set();
this.reset();
};
/******************************************************************************/
// Reset all, thus reducing to a minimum memory footprint of the context.
CosmeticFilteringEngine.prototype.reset = function() {
this.frozen = false;
this.acceptedCount = 0;
this.discardedCount = 0;
this.duplicateBuster = new Set();
this.selectorCache.clear();
this.selectorCacheTimer.off();
// hostname, entity-based filters
this.specificFilters.clear();
// low generic cosmetic filters
this.lowlyGeneric.clear();
// highly generic selectors sets
this.highlyGeneric.simple.dict.clear();
this.highlyGeneric.simple.str = '';
this.highlyGeneric.simple.mru.reset();
this.highlyGeneric.complex.dict.clear();
this.highlyGeneric.complex.str = '';
this.highlyGeneric.complex.mru.reset();
this.selfieVersion = 2;
};
/******************************************************************************/
CosmeticFilteringEngine.prototype.freeze = function() {
this.duplicateBuster.clear();
this.specificFilters.collectGarbage();
this.highlyGeneric.simple.str = Array.from(this.highlyGeneric.simple.dict).join(',\n');
this.highlyGeneric.simple.mru.reset();
this.highlyGeneric.complex.str = Array.from(this.highlyGeneric.complex.dict).join(',\n');
this.highlyGeneric.complex.mru.reset();
this.frozen = true;
};
/******************************************************************************/
CosmeticFilteringEngine.prototype.compile = function(parser, writer) {
if ( parser.hasOptions() === false ) {
this.compileGenericSelector(parser, writer);
return true;
}
// https://github.com/chrisaljoudi/uBlock/issues/151
// Negated hostname means the filter applies to all non-negated hostnames
// of same filter OR globally if there is no non-negated hostnames.
let applyGlobally = true;
for ( const { hn, not, bad } of parser.getExtFilterDomainIterator() ) {
if ( bad ) { continue; }
if ( not === false ) {
applyGlobally = false;
}
this.compileSpecificSelector(parser, hn, not, writer);
}
if ( applyGlobally ) {
this.compileGenericSelector(parser, writer);
}
return true;
};
/******************************************************************************/
CosmeticFilteringEngine.prototype.compileGenericSelector = function(parser, writer) {
if ( parser.isException() ) {
this.compileGenericUnhideSelector(parser, writer);
} else {
this.compileGenericHideSelector(parser, writer);
}
};
/******************************************************************************/
CosmeticFilteringEngine.prototype.compileGenericHideSelector = function(
parser,
writer
) {
const { raw, compiled } = parser.result;
if ( compiled === undefined ) {
const who = writer.properties.get('name') || '?';
logger.writeOne({
realm: 'message',
type: 'error',
text: `Invalid generic cosmetic filter in ${who}: ${raw}`
});
return;
}
writer.select('COSMETIC_FILTERS:GENERIC');
// https://github.com/uBlockOrigin/uBlock-issues/issues/131
// Support generic procedural filters as per advanced settings.
if ( compiled.charCodeAt(0) === 0x7B /* '{' */ ) {
if ( µb.hiddenSettings.allowGenericProceduralFilters === true ) {
return this.compileSpecificSelector(parser, '', false, writer);
}
const who = writer.properties.get('name') || '?';
logger.writeOne({
realm: 'message',
type: 'error',
text: `Invalid generic cosmetic filter in ${who}: ##${raw}`
});
return;
}
const key = keyFromSelector(compiled);
if ( key !== undefined ) {
writer.push([
0,
hashFromStr(key.charCodeAt(0), key.slice(1)),
compiled,
]);
return;
}
// Pass this point, we are dealing with highly-generic cosmetic filters.
//
// For efficiency purpose, we will distinguish between simple and complex
// selectors.
if ( this.reSimpleHighGeneric.test(compiled) ) {
writer.push([ 4 /* simple */, compiled ]);
} else {
writer.push([ 5 /* complex */, compiled ]);
}
};
/******************************************************************************/
CosmeticFilteringEngine.prototype.compileGenericUnhideSelector = function(
parser,
writer
) {
// Procedural cosmetic filters are acceptable as generic exception filters.
const { raw, compiled } = parser.result;
if ( compiled === undefined ) {
const who = writer.properties.get('name') || '?';
logger.writeOne({
realm: 'message',
type: 'error',
text: `Invalid cosmetic filter in ${who}: #@#${raw}`
});
return;
}
writer.select('COSMETIC_FILTERS:SPECIFIC');
// https://github.com/chrisaljoudi/uBlock/issues/497
// All generic exception filters are stored as hostname-based filter
// whereas the hostname is the empty string (which matches all
// hostnames). No distinction is made between declarative and
// procedural selectors, since they really exist only to cancel
// out other cosmetic filters.
writer.push([ 8, '', 0b001, compiled ]);
};
/******************************************************************************/
CosmeticFilteringEngine.prototype.compileSpecificSelector = function(
parser,
hostname,
not,
writer
) {
const { raw, compiled, exception } = parser.result;
if ( compiled === undefined ) {
const who = writer.properties.get('name') || '?';
logger.writeOne({
realm: 'message',
type: 'error',
text: `Invalid cosmetic filter in ${who}: ##${raw}`
});
return;
}
writer.select('COSMETIC_FILTERS:SPECIFIC');
// https://github.com/chrisaljoudi/uBlock/issues/145
let unhide = exception ? 1 : 0;
if ( not ) { unhide ^= 1; }
let kind = 0;
if ( unhide === 1 ) {
kind |= 0b001; // Exception
}
if ( compiled.charCodeAt(0) === 0x7B /* '{' */ ) {
kind |= 0b010; // Procedural
}
if ( hostname === '*' ) {
kind |= 0b100; // Applies everywhere
}
writer.push([ 8, hostname, kind, compiled ]);
};
/******************************************************************************/
CosmeticFilteringEngine.prototype.fromCompiledContent = function(reader, options) {
if ( options.skipCosmetic ) {
this.skipCompiledContent(reader, 'SPECIFIC');
this.skipCompiledContent(reader, 'GENERIC');
return;
}
// Specific cosmetic filter section
reader.select('COSMETIC_FILTERS:SPECIFIC');
while ( reader.next() ) {
this.acceptedCount += 1;
const fingerprint = reader.fingerprint();
if ( this.duplicateBuster.has(fingerprint) ) {
this.discardedCount += 1;
continue;
}
this.duplicateBuster.add(fingerprint);
const args = reader.args();
switch ( args[0] ) {
// hash, example.com, .promoted-tweet
// hash, example.*, .promoted-tweet
//
// https://github.com/uBlockOrigin/uBlock-issues/issues/803
// Handle specific filters meant to apply everywhere, i.e. selectors
// not to be injected conditionally through the DOM surveyor.
// hash, *, .promoted-tweet
case 8:
if ( args[2] === 0b100 ) {
if ( this.reSimpleHighGeneric.test(args[3]) )
this.highlyGeneric.simple.dict.add(args[3]);
else {
this.highlyGeneric.complex.dict.add(args[3]);
}
break;
}
this.specificFilters.store(args[1], args[2] & 0b011, args[3]);
break;
default:
this.discardedCount += 1;
break;
}
}
if ( options.skipGenericCosmetic ) {
this.skipCompiledContent(reader, 'GENERIC');
return;
}
// Generic cosmetic filter section
reader.select('COSMETIC_FILTERS:GENERIC');
while ( reader.next() ) {
this.acceptedCount += 1;
const fingerprint = reader.fingerprint();
if ( this.duplicateBuster.has(fingerprint) ) {
this.discardedCount += 1;
continue;
}
this.duplicateBuster.add(fingerprint);
const args = reader.args();
switch ( args[0] ) {
// low generic
case 0: {
if ( this.lowlyGeneric.has(args[1]) ) {
const selector = this.lowlyGeneric.get(args[1]);
this.lowlyGeneric.set(args[1], `${selector},\n${args[2]}`);
} else {
this.lowlyGeneric.set(args[1], args[2]);
}
break;
}
// High-high generic hide/simple selectors
// div[id^="allo"]
case 4:
this.highlyGeneric.simple.dict.add(args[1]);
break;
// High-high generic hide/complex selectors
// div[id^="allo"] > span
case 5:
this.highlyGeneric.complex.dict.add(args[1]);
break;
default:
this.discardedCount += 1;
break;
}
}
};
/******************************************************************************/
CosmeticFilteringEngine.prototype.skipCompiledContent = function(reader, sectionId) {
reader.select(`COSMETIC_FILTERS:${sectionId}`);
while ( reader.next() ) {
this.acceptedCount += 1;
this.discardedCount += 1;
}
};
/******************************************************************************/
CosmeticFilteringEngine.prototype.toSelfie = function() {
return {
version: this.selfieVersion,
acceptedCount: this.acceptedCount,
discardedCount: this.discardedCount,
specificFilters: this.specificFilters.toSelfie(),
lowlyGeneric: this.lowlyGeneric,
highSimpleGenericHideDict: this.highlyGeneric.simple.dict,
highSimpleGenericHideStr: this.highlyGeneric.simple.str,
highComplexGenericHideDict: this.highlyGeneric.complex.dict,
highComplexGenericHideStr: this.highlyGeneric.complex.str,
};
};
/******************************************************************************/
CosmeticFilteringEngine.prototype.fromSelfie = function(selfie) {
if ( selfie.version !== this.selfieVersion ) {
throw new Error(
`cosmeticFilteringEngine: mismatched selfie version, ${selfie.version}, expected ${this.selfieVersion}`
);
}
this.acceptedCount = selfie.acceptedCount;
this.discardedCount = selfie.discardedCount;
this.specificFilters.fromSelfie(selfie.specificFilters);
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;
};
/******************************************************************************/
CosmeticFilteringEngine.prototype.addToSelectorCache = function(details) {
const hostname = details.hostname;
if ( typeof hostname !== 'string' || hostname === '' ) { return; }
const selectors = details.selectors;
if ( Array.isArray(selectors) === false ) { return; }
let entry = this.selectorCache.get(hostname);
if ( entry === undefined ) {
entry = SelectorCacheEntry.factory();
this.selectorCache.set(hostname, entry);
if ( this.selectorCache.size > this.selectorCacheCountMax ) {
this.selectorCacheTimer.on({ min: this.selectorCachePruneDelay });
}
}
entry.add(details);
};
/******************************************************************************/
CosmeticFilteringEngine.prototype.removeFromSelectorCache = function(
targetHostname = '*',
type = undefined
) {
const targetHostnameLength = targetHostname.length;
for ( let entry of this.selectorCache ) {
let hostname = entry[0];
let item = entry[1];
if ( targetHostname !== '*' ) {
if ( hostname.endsWith(targetHostname) === false ) { continue; }
if (
hostname.length !== targetHostnameLength &&
hostname.charAt(hostname.length - targetHostnameLength - 1) !== '.'
) {
continue;
}
}
item.remove(type);
}
};
/******************************************************************************/
CosmeticFilteringEngine.prototype.pruneSelectorCacheAsync = function() {
if ( this.selectorCache.size <= this.selectorCacheCountMax ) { return; }
const cache = this.selectorCache;
const hostnames = Array.from(cache.keys())
.sort((a, b) => cache.get(b).accessId - cache.get(a).accessId)
.slice(this.selectorCacheCountMin);
for ( const hn of hostnames ) {
cache.get(hn).dispose();
cache.delete(hn);
}
};
/******************************************************************************/
CosmeticFilteringEngine.prototype.disableSurveyor = function(details) {
const hostname = details.hostname;
if ( typeof hostname !== 'string' || hostname === '' ) { return; }
const cacheEntry = this.selectorCache.get(hostname);
if ( cacheEntry === undefined ) { return; }
cacheEntry.disableSurveyor = true;
};
/******************************************************************************/
CosmeticFilteringEngine.prototype.cssRuleFromProcedural = function(pfilter) {
if ( pfilter.cssable !== true ) { return; }
const { tasks, action } = pfilter;
let mq, selector;
if ( Array.isArray(tasks) ) {
if ( tasks[0][0] !== 'matches-media' ) { return; }
mq = tasks[0][1];
if ( tasks.length > 2 ) { return; }
if ( tasks.length === 2 ) {
if ( tasks[1][0] !== 'spath' ) { return; }
selector = tasks[1][1];
}
}
let style;
if ( Array.isArray(action) ) {
if ( action[0] !== 'style' ) { return; }
selector = selector || pfilter.selector;
style = action[1];
}
if ( mq === undefined && style === undefined && selector === undefined ) { return; }
if ( mq === undefined ) {
return `${selector}\n{${style}}`;
}
if ( style === undefined ) {
return `@media ${mq} {\n${selector}\n{display:none!important;}\n}`;
}
return `@media ${mq} {\n${selector}\n{${style}}\n}`;
};
/******************************************************************************/
CosmeticFilteringEngine.prototype.retrieveGenericSelectors = function(request) {
if ( this.lowlyGeneric.size === 0 ) { return; }
if ( Array.isArray(request.hashes) === false ) { return; }
if ( request.hashes.length === 0 ) { return; }
const selectorsSet = new Set();
const hashes = [];
const safeOnly = request.safeOnly === true;
for ( const hash of request.hashes ) {
const bucket = this.lowlyGeneric.get(hash);
if ( bucket === undefined ) { continue; }
for ( const selector of bucket.split(',\n') ) {
if ( safeOnly && selector === keyFromSelector(selector) ) { continue; }
selectorsSet.add(selector);
}
hashes.push(hash);
}
// Apply exceptions: it is the responsibility of the caller to provide
// the exceptions to be applied.
const excepted = [];
if ( selectorsSet.size !== 0 && Array.isArray(request.exceptions) ) {
for ( const exception of request.exceptions ) {
if ( selectorsSet.delete(exception) ) {
excepted.push(exception);
}
}
}
if ( selectorsSet.size === 0 && excepted.length === 0 ) { return; }
const out = { injectedCSS: '', excepted, };
const selectors = Array.from(selectorsSet);
if ( typeof request.hostname === 'string' && request.hostname !== '' ) {
this.addToSelectorCache({
hostname: request.hostname,
selectors,
hashes,
type: 'cosmetic',
});
}
if ( selectors.length === 0 ) { return out; }
out.injectedCSS = `${selectors.join(',\n')}\n{display:none!important;}`;
vAPI.tabs.insertCSS(request.tabId, {
code: out.injectedCSS,
frameId: request.frameId,
matchAboutBlank: true,
runAt: 'document_start',
});
return out;
};
/******************************************************************************/
CosmeticFilteringEngine.prototype.retrieveSpecificSelectors = function(
request,
options
) {
const hostname = request.hostname;
const cacheEntry = this.selectorCache.get(hostname);
// https://github.com/chrisaljoudi/uBlock/issues/587
// out.ready will tell the content script the cosmetic filtering engine is
// up and ready.
// https://github.com/chrisaljoudi/uBlock/issues/497
// Generic exception filters are to be applied on all pages.
const out = {
ready: this.frozen,
hostname: hostname,
domain: request.domain,
exceptionFilters: [],
exceptedFilters: [],
proceduralFilters: [],
convertedProceduralFilters: [],
disableSurveyor: this.lowlyGeneric.size === 0,
};
const injectedCSS = [];
if (
options.noSpecificCosmeticFiltering !== true ||
options.noGenericCosmeticFiltering !== true
) {
const specificSet = this.$specificSet;
const proceduralSet = this.$proceduralSet;
const exceptionSet = this.$exceptionSet;
const dummySet = this.$dummySet;
// Cached cosmetic filters: these are always declarative.
if ( cacheEntry !== undefined ) {
cacheEntry.retrieveCosmetic(specificSet, out.genericCosmeticHashes = []);
if ( cacheEntry.disableSurveyor ) {
out.disableSurveyor = true;
}
}
// Retrieve filters with a non-empty hostname
const retrieveSets = [ specificSet, exceptionSet, proceduralSet, exceptionSet ];
const discardSets = [ dummySet, exceptionSet ];
this.specificFilters.retrieve(
hostname,
options.noSpecificCosmeticFiltering ? discardSets : retrieveSets,
1
);
// Retrieve filters with a regex-based hostname value
this.specificFilters.retrieve(
hostname,
options.noSpecificCosmeticFiltering ? discardSets : retrieveSets,
3
);
// Retrieve filters with a entity-based hostname value
if ( request.entity !== '' ) {
this.specificFilters.retrieve(
`${hostname.slice(0, -request.domain.length)}${request.entity}`,
options.noSpecificCosmeticFiltering ? discardSets : retrieveSets,
1
);
}
// Retrieve filters with an empty hostname
this.specificFilters.retrieve(
hostname,
options.noGenericCosmeticFiltering ? discardSets : retrieveSets,
2
);
// Apply exceptions to specific filterset
if ( exceptionSet.size !== 0 ) {
out.exceptionFilters = Array.from(exceptionSet);
for ( const selector of specificSet ) {
if ( exceptionSet.has(selector) === false ) { continue; }
specificSet.delete(selector);
out.exceptedFilters.push(selector);
}
}
if ( specificSet.size !== 0 ) {
injectedCSS.push(
`${Array.from(specificSet).join(',\n')}\n{display:none!important;}`
);
}
// Apply exceptions to procedural filterset.
// Also, some procedural filters are really declarative cosmetic
// filters, so we extract and inject them immediately.
if ( proceduralSet.size !== 0 ) {
for ( const json of proceduralSet ) {
const pfilter = JSON.parse(json);
if ( exceptionSet.has(json) ) {
proceduralSet.delete(json);
out.exceptedFilters.push(json);
continue;
}
if ( exceptionSet.has(pfilter.raw) ) {
proceduralSet.delete(json);
out.exceptedFilters.push(pfilter.raw);
continue;
}
const cssRule = this.cssRuleFromProcedural(pfilter);
if ( cssRule === undefined ) { continue; }
injectedCSS.push(cssRule);
proceduralSet.delete(json);
out.convertedProceduralFilters.push(json);
}
out.proceduralFilters.push(...proceduralSet);
}
// Highly generic cosmetic filters: sent once along with specific ones.
// A most-recent-used cache is used to skip computing the resulting set
// of high generics for a given set of exceptions.
// The resulting set of high generics is stored as a string, ready to
// be used as-is by the content script. The string is stored
// indirectly in the mru cache: this is to prevent duplication of the
// string in memory, which I have observed occurs when the string is
// stored directly as a value in a Map.
if ( options.noGenericCosmeticFiltering !== true ) {
const exceptionSetHash = out.exceptionFilters.join();
for ( const key in this.highlyGeneric ) {
const entry = this.highlyGeneric[key];
let str = entry.mru.lookup(exceptionSetHash);
if ( str === undefined ) {
str = { s: entry.str, excepted: [] };
let genericSet = entry.dict;
let hit = false;
for ( const exception of exceptionSet ) {
if ( (hit = genericSet.has(exception)) ) { break; }
}
if ( hit ) {
genericSet = new Set(entry.dict);
for ( const exception of exceptionSet ) {
if ( genericSet.delete(exception) ) {
str.excepted.push(exception);
}
}
str.s = Array.from(genericSet).join(',\n');
}
entry.mru.add(exceptionSetHash, str);
}
if ( str.excepted.length !== 0 ) {
out.exceptedFilters.push(...str.excepted);
}
if ( str.s.length !== 0 ) {
injectedCSS.push(`${str.s}\n{display:none!important;}`);
}
}
}
// Important: always clear used registers before leaving.
specificSet.clear();
proceduralSet.clear();
exceptionSet.clear();
dummySet.clear();
}
const details = {
code: '',
frameId: request.frameId,
matchAboutBlank: true,
runAt: 'document_start',
};
// Inject all declarative-based filters as a single stylesheet.
if ( injectedCSS.length !== 0 ) {
out.injectedCSS = injectedCSS.join('\n\n');
details.code = out.injectedCSS;
if ( request.tabId !== undefined && options.dontInject !== true ) {
vAPI.tabs.insertCSS(request.tabId, details);
}
}
// CSS selectors for collapsible blocked elements
if ( cacheEntry ) {
const networkFilters = [];
if ( cacheEntry.retrieveNet(networkFilters) ) {
details.code = `${networkFilters.join('\n')}\n{display:none!important;}`;
if ( request.tabId !== undefined && options.dontInject !== true ) {
vAPI.tabs.insertCSS(request.tabId, details);
}
}
}
return out;
};
/******************************************************************************/
CosmeticFilteringEngine.prototype.getFilterCount = function() {
return this.acceptedCount - this.discardedCount;
};
/******************************************************************************/
CosmeticFilteringEngine.prototype.dump = function() {
const lowlyGenerics = [];
for ( const selectors of this.lowlyGeneric.values() ) {
lowlyGenerics.push(...selectors.split(',\n'));
}
lowlyGenerics.sort();
const highlyGenerics = Array.from(this.highlyGeneric.simple.dict).sort();
highlyGenerics.push(...Array.from(this.highlyGeneric.complex.dict).sort());
return [
'Cosmetic Filtering Engine internals:',
`specific: ${this.specificFilters.size}`,
`generic: ${lowlyGenerics.length + highlyGenerics.length}`,
`+ lowly generic: ${lowlyGenerics.length}`,
...lowlyGenerics.map(a => ` ${a}`),
`+ highly generic: ${highlyGenerics.length}`,
...highlyGenerics.map(a => ` ${a}`),
].join('\n');
};
/******************************************************************************/
const cosmeticFilteringEngine = new CosmeticFilteringEngine();
export default cosmeticFilteringEngine;
/******************************************************************************/

View File

@@ -0,0 +1,215 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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';
import { dom } from './dom.js';
/******************************************************************************/
self.uBlockDashboard = self.uBlockDashboard || {};
/******************************************************************************/
// Helper for client panes:
// Remove literal duplicate lines from a set based on another set.
self.uBlockDashboard.mergeNewLines = function(text, newText) {
// Step 1: build dictionary for existing lines.
const fromDict = new Map();
let lineBeg = 0;
let textEnd = text.length;
while ( lineBeg < textEnd ) {
let lineEnd = text.indexOf('\n', lineBeg);
if ( lineEnd === -1 ) {
lineEnd = text.indexOf('\r', lineBeg);
if ( lineEnd === -1 ) {
lineEnd = textEnd;
}
}
const line = text.slice(lineBeg, lineEnd).trim();
lineBeg = lineEnd + 1;
if ( line.length === 0 ) { continue; }
const hash = line.slice(0, 8);
const bucket = fromDict.get(hash);
if ( bucket === undefined ) {
fromDict.set(hash, line);
} else if ( typeof bucket === 'string' ) {
fromDict.set(hash, [ bucket, line ]);
} else /* if ( Array.isArray(bucket) ) */ {
bucket.push(line);
}
}
// Step 2: use above dictionary to filter out duplicate lines.
const out = [ '' ];
lineBeg = 0;
textEnd = newText.length;
while ( lineBeg < textEnd ) {
let lineEnd = newText.indexOf('\n', lineBeg);
if ( lineEnd === -1 ) {
lineEnd = newText.indexOf('\r', lineBeg);
if ( lineEnd === -1 ) {
lineEnd = textEnd;
}
}
const line = newText.slice(lineBeg, lineEnd).trim();
lineBeg = lineEnd + 1;
if ( line.length === 0 ) {
if ( out[out.length - 1] !== '' ) {
out.push('');
}
continue;
}
const bucket = fromDict.get(line.slice(0, 8));
if ( bucket === undefined ) {
out.push(line);
continue;
}
if ( typeof bucket === 'string' && line !== bucket ) {
out.push(line);
continue;
}
if ( bucket.indexOf(line) === -1 ) {
out.push(line);
/* continue; */
}
}
const append = out.join('\n').trim();
if ( text !== '' && append !== '' ) {
text += '\n\n';
}
return text + append;
};
/******************************************************************************/
self.uBlockDashboard.dateNowToSensibleString = function() {
const now = new Date(Date.now() - (new Date()).getTimezoneOffset() * 60000);
return now.toISOString().replace(/\.\d+Z$/, '')
.replace(/:/g, '.')
.replace('T', '_');
};
/******************************************************************************/
self.uBlockDashboard.patchCodeMirrorEditor = (function() {
let grabFocusTarget;
const grabFocus = function() {
grabFocusTarget.focus();
grabFocusTarget = undefined;
};
const grabFocusTimer = vAPI.defer.create(grabFocus);
const grabFocusAsync = function(cm) {
grabFocusTarget = cm;
grabFocusTimer.on(1);
};
// https://github.com/gorhill/uBlock/issues/3646
const patchSelectAll = function(cm, details) {
var vp = cm.getViewport();
if ( details.ranges.length !== 1 ) { return; }
var range = details.ranges[0],
lineFrom = range.anchor.line,
lineTo = range.head.line;
if ( lineTo === lineFrom ) { return; }
if ( range.head.ch !== 0 ) { lineTo += 1; }
if ( lineFrom !== vp.from || lineTo !== vp.to ) { return; }
details.update([
{
anchor: { line: 0, ch: 0 },
head: { line: cm.lineCount(), ch: 0 }
}
]);
grabFocusAsync(cm);
};
let lastGutterClick = 0;
let lastGutterLine = 0;
const onGutterClicked = function(cm, line, gutter) {
if ( gutter !== 'CodeMirror-linenumbers' ) { return; }
grabFocusAsync(cm);
const delta = Date.now() - lastGutterClick;
// Single click
if ( delta >= 500 || line !== lastGutterLine ) {
cm.setSelection(
{ line, ch: 0 },
{ line: line + 1, ch: 0 }
);
lastGutterClick = Date.now();
lastGutterLine = line;
return;
}
// Double click: select fold-able block or all
let lineFrom = 0;
let lineTo = cm.lineCount();
const foldFn = cm.getHelper({ line, ch: 0 }, 'fold');
if ( foldFn instanceof Function ) {
const range = foldFn(cm, { line, ch: 0 });
if ( range !== undefined ) {
lineFrom = range.from.line;
lineTo = range.to.line + 1;
}
}
cm.setSelection(
{ line: lineFrom, ch: 0 },
{ line: lineTo, ch: 0 },
{ scroll: false }
);
lastGutterClick = 0;
};
return function(cm) {
if ( cm.options.inputStyle === 'contenteditable' ) {
cm.on('beforeSelectionChange', patchSelectAll);
}
cm.on('gutterClick', onGutterClicked);
};
})();
/******************************************************************************/
self.uBlockDashboard.openOrSelectPage = function(url, options = {}) {
let ev;
if ( url instanceof MouseEvent ) {
ev = url;
url = dom.attr(ev.target, 'href');
}
const details = Object.assign({ url, select: true, index: -1 }, options);
vAPI.messaging.send('default', {
what: 'gotoURL',
details,
});
if ( ev ) {
ev.preventDefault();
}
};
/******************************************************************************/
// Open links in the proper window
dom.attr('a', 'target', '_blank');
dom.attr('a[href*="dashboard.html"]', 'target', '_parent');

View File

@@ -0,0 +1,170 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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';
import { dom, qs$ } from './dom.js';
/******************************************************************************/
function discardUnsavedData(synchronous = false) {
const paneFrame = qs$('#iframe');
const paneWindow = paneFrame.contentWindow;
if (
typeof paneWindow.hasUnsavedData !== 'function' ||
paneWindow.hasUnsavedData() === false
) {
return true;
}
if ( synchronous ) {
return false;
}
return new Promise(resolve => {
const modal = qs$('#unsavedWarning');
dom.cl.add(modal, 'on');
modal.focus();
const onDone = status => {
dom.cl.remove(modal, 'on');
dom.off(document, 'click', onClick, true);
resolve(status);
};
const onClick = ev => {
const target = ev.target;
if ( target.matches('[data-i18n="dashboardUnsavedWarningStay"]') ) {
return onDone(false);
}
if ( target.matches('[data-i18n="dashboardUnsavedWarningIgnore"]') ) {
return onDone(true);
}
if ( qs$(modal, '[data-i18n="dashboardUnsavedWarning"]').contains(target) ) {
return;
}
onDone(false);
};
dom.on(document, 'click', onClick, true);
});
}
function loadDashboardPanel(pane, first) {
const tabButton = qs$(`[data-pane="${pane}"]`);
if ( tabButton === null || dom.cl.has(tabButton, 'selected') ) { return; }
const loadPane = ( ) => {
self.location.replace(`#${pane}`);
dom.cl.remove('.tabButton.selected', 'selected');
dom.cl.add(tabButton, 'selected');
tabButton.scrollIntoView();
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);
}
};
if ( first ) {
return loadPane();
}
const r = discardUnsavedData();
if ( r === false ) { return; }
if ( r === true ) { return loadPane(); }
r.then(status => {
if ( status === false ) { return; }
loadPane();
});
}
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');
}
(async ( ) => {
// Wait for uBO's main process to be ready
await new Promise(resolve => {
const check = async ( ) => {
try {
const response = await vAPI.messaging.send('dashboard', {
what: 'readyToFilter'
});
if ( response ) { return resolve(true); }
const iframe = qs$('#iframe');
if ( iframe.src !== '' ) {
iframe.src = '';
}
} catch(ex) {
}
vAPI.defer.once(250).then(( ) => check());
};
check();
});
dom.cl.remove(dom.body, 'notReady');
const results = await Promise.all([
// https://github.com/uBlockOrigin/uBlock-issues/issues/106
vAPI.messaging.send('dashboard', { what: 'dashboardConfig' }),
vAPI.localStorage.getItemAsync('dashboardLastVisitedPane'),
]);
{
const details = results[0] || {};
if ( details.noDashboard ) {
self.location.hash = '#no-dashboard.html';
dom.cl.add(dom.body, 'noDashboard');
} else if ( self.location.hash === '#no-dashboard.html' ) {
self.location.hash = '';
}
}
{
let pane = results[1] || null;
if ( self.location.hash !== '' ) {
pane = self.location.hash.slice(1) || null;
}
loadDashboardPanel(pane !== null ? pane : 'settings.html', true);
dom.on('.tabButton', 'click', onTabClickHandler);
// https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
dom.on(self, 'beforeunload', ( ) => {
if ( discardUnsavedData(true) ) { return; }
event.preventDefault();
event.returnValue = '';
});
// https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
dom.on(self, 'hashchange', ( ) => {
const pane = self.location.hash.slice(1);
if ( pane === '' ) { return; }
loadDashboardPanel(pane);
});
}
})();

View File

@@ -0,0 +1,348 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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
*/
/* global CodeMirror, uBlockDashboard */
import * as s14e from './s14e-serializer.js';
import { dom, qs$ } from './dom.js';
/******************************************************************************/
const reFoldable = /^ *(?=\+ \S)/;
/******************************************************************************/
CodeMirror.registerGlobalHelper(
'fold',
'ubo-dump',
( ) => true,
(cm, start) => {
const startLineNo = start.line;
const startLine = cm.getLine(startLineNo);
let endLineNo = startLineNo;
let endLine = startLine;
const match = reFoldable.exec(startLine);
if ( match === null ) { return; }
const foldCandidate = ' ' + match[0];
const lastLineNo = cm.lastLine();
let nextLineNo = startLineNo + 1;
while ( nextLineNo < lastLineNo ) {
const nextLine = cm.getLine(nextLineNo);
// TODO: use regex to find folding end
if ( nextLine.startsWith(foldCandidate) === false && nextLine !== ']' ) {
if ( startLineNo >= endLineNo ) { return; }
return {
from: CodeMirror.Pos(startLineNo, startLine.length),
to: CodeMirror.Pos(endLineNo, endLine.length)
};
}
endLine = nextLine;
endLineNo = nextLineNo;
nextLineNo += 1;
}
}
);
const cmEditor = new CodeMirror(qs$('#console'), {
autofocus: true,
foldGutter: true,
gutters: [ 'CodeMirror-linenumbers', 'CodeMirror-foldgutter' ],
lineNumbers: true,
lineWrapping: true,
mode: 'ubo-dump',
styleActiveLine: true,
undoDepth: 5,
});
uBlockDashboard.patchCodeMirrorEditor(cmEditor);
/******************************************************************************/
function log(text) {
cmEditor.replaceRange(text.trim() + '\n\n', { line: 0, ch: 0 });
}
/******************************************************************************/
function toDNRText(raw) {
const result = s14e.deserialize(raw);
if ( typeof result === 'string' ) { return result; }
const { network } = result;
const replacer = (k, v) => {
if ( k.startsWith('__') ) { return; }
if ( Array.isArray(v) ) {
return v.sort();
}
if ( v instanceof Object ) {
const sorted = {};
for ( const kk of Object.keys(v).sort() ) {
sorted[kk] = v[kk];
}
return sorted;
}
return v;
};
const isUnsupported = rule =>
rule._error !== undefined;
const isRegex = rule =>
rule.condition !== undefined &&
rule.condition.regexFilter !== undefined;
const isRedirect = rule =>
rule.action !== undefined &&
rule.action.type === 'redirect' &&
rule.action.redirect.extensionPath !== undefined;
const isCsp = rule =>
rule.action !== undefined &&
rule.action.type === 'modifyHeaders';
const isRemoveparam = rule =>
rule.action !== undefined &&
rule.action.type === 'redirect' &&
rule.action.redirect.transform !== undefined;
const { ruleset } = network;
const good = ruleset.filter(rule =>
isUnsupported(rule) === false &&
isRegex(rule) === false &&
isRedirect(rule) === false &&
isCsp(rule) === false &&
isRemoveparam(rule) === false
);
const unsupported = ruleset.filter(rule =>
isUnsupported(rule)
);
const regexes = ruleset.filter(rule =>
isUnsupported(rule) === false &&
isRegex(rule) &&
isRedirect(rule) === false &&
isCsp(rule) === false &&
isRemoveparam(rule) === false
);
const redirects = ruleset.filter(rule =>
isUnsupported(rule) === false &&
isRedirect(rule)
);
const headers = ruleset.filter(rule =>
isUnsupported(rule) === false &&
isCsp(rule)
);
const removeparams = ruleset.filter(rule =>
isUnsupported(rule) === false &&
isRemoveparam(rule)
);
const out = [
`dnrRulesetFromRawLists(${JSON.stringify(result.listNames, null, 2)})`,
`Run time: ${result.runtime} ms`,
`Filters count: ${network.filterCount}`,
`Accepted filter count: ${network.acceptedFilterCount}`,
`Rejected filter count: ${network.rejectedFilterCount}`,
`Un-DNR-able filter count: ${unsupported.length}`,
`Resulting DNR rule count: ${ruleset.length}`,
];
out.push(`+ Good filters (${good.length}): ${JSON.stringify(good, replacer, 2)}`);
out.push(`+ Regex-based filters (${regexes.length}): ${JSON.stringify(regexes, replacer, 2)}`);
out.push(`+ 'redirect=' filters (${redirects.length}): ${JSON.stringify(redirects, replacer, 2)}`);
out.push(`+ 'csp=' filters (${headers.length}): ${JSON.stringify(headers, replacer, 2)}`);
out.push(`+ 'removeparam=' filters (${removeparams.length}): ${JSON.stringify(removeparams, replacer, 2)}`);
out.push(`+ Unsupported filters (${unsupported.length}): ${JSON.stringify(unsupported, replacer, 2)}`);
out.push(`+ generichide exclusions (${network.generichideExclusions.length}): ${JSON.stringify(network.generichideExclusions, replacer, 2)}`);
if ( result.specificCosmetic ) {
out.push(`+ Cosmetic filters: ${result.specificCosmetic.size}`);
for ( const details of result.specificCosmetic ) {
out.push(` ${JSON.stringify(details)}`);
}
} else {
out.push(' Cosmetic filters: 0');
}
return out.join('\n');
}
/******************************************************************************/
dom.on('#console-clear', 'click', ( ) => {
cmEditor.setValue('');
});
dom.on('#console-fold', 'click', ( ) => {
const unfolded = [];
let maxUnfolded = -1;
cmEditor.eachLine(handle => {
const match = reFoldable.exec(handle.text);
if ( match === null ) { return; }
const depth = match[0].length;
const line = handle.lineNo();
const isFolded = cmEditor.isFolded({ line, ch: handle.text.length });
if ( isFolded === true ) { return; }
unfolded.push({ line, depth });
maxUnfolded = Math.max(maxUnfolded, depth);
});
if ( maxUnfolded === -1 ) { return; }
cmEditor.startOperation();
for ( const details of unfolded ) {
if ( details.depth !== maxUnfolded ) { continue; }
cmEditor.foldCode(details.line, null, 'fold');
}
cmEditor.endOperation();
});
dom.on('#console-unfold', 'click', ( ) => {
const folded = [];
let minFolded = Number.MAX_SAFE_INTEGER;
cmEditor.eachLine(handle => {
const match = reFoldable.exec(handle.text);
if ( match === null ) { return; }
const depth = match[0].length;
const line = handle.lineNo();
const isFolded = cmEditor.isFolded({ line, ch: handle.text.length });
if ( isFolded !== true ) { return; }
folded.push({ line, depth });
minFolded = Math.min(minFolded, depth);
});
if ( minFolded === Number.MAX_SAFE_INTEGER ) { return; }
cmEditor.startOperation();
for ( const details of folded ) {
if ( details.depth !== minFolded ) { continue; }
cmEditor.foldCode(details.line, null, 'unfold');
}
cmEditor.endOperation();
});
dom.on('#snfe-dump', 'click', ev => {
const button = ev.target;
dom.attr(button, 'disabled', '');
vAPI.messaging.send('devTools', {
what: 'snfeDump',
}).then(result => {
log(result);
dom.attr(button, 'disabled', null);
});
});
dom.on('#snfe-todnr', 'click', ev => {
const button = ev.target;
dom.attr(button, 'disabled', '');
vAPI.messaging.send('devTools', {
what: 'snfeToDNR',
}).then(result => {
log(toDNRText(result));
dom.attr(button, 'disabled', null);
});
});
dom.on('#cfe-dump', 'click', ev => {
const button = ev.target;
dom.attr(button, 'disabled', '');
vAPI.messaging.send('devTools', {
what: 'cfeDump',
}).then(result => {
log(result);
dom.attr(button, 'disabled', null);
});
});
dom.on('#purge-all-caches', 'click', ( ) => {
vAPI.messaging.send('devTools', {
what: 'purgeAllCaches'
}).then(result => {
log(result);
});
});
vAPI.messaging.send('dashboard', {
what: 'getAppData',
}).then(appData => {
if ( appData.canBenchmark !== true ) { return; }
dom.attr('#snfe-benchmark', 'disabled', null);
dom.on('#snfe-benchmark', 'click', ev => {
const button = ev.target;
dom.attr(button, 'disabled', '');
vAPI.messaging.send('devTools', {
what: 'snfeBenchmark',
}).then(result => {
log(result);
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);
});
});
});
/******************************************************************************/
async function snfeQuery(lineNo, query) {
const doc = cmEditor.getDoc();
const lineHandle = doc.getLineHandle(lineNo)
const result = await vAPI.messaging.send('devTools', {
what: 'snfeQuery',
query
});
if ( typeof result !== 'string' ) { return; }
cmEditor.startOperation();
const nextLineNo = doc.getLineNumber(lineHandle) + 1;
doc.replaceRange(`${result}\n`, { line: nextLineNo, ch: 0 });
cmEditor.endOperation();
}
cmEditor.on('beforeChange', (cm, details) => {
if ( details.origin !== '+input' ) { return; }
if ( details.text.length !== 2 ) { return; }
if ( details.text[1] !== '' ) { return; }
const lineNo = details.from.line;
const line = cm.getLine(lineNo);
if ( details.from.ch !== line.length ) { return; }
if ( line.startsWith('snfe?') === false ) { return; }
const fields = line.slice(5).split(/\s+/);
const query = {};
for ( const field of fields ) {
if ( /[/.]/.test(field) ) {
if ( query.url === undefined ) {
query.url = field;
} else if ( query.from === undefined ) {
query.from = field;
}
} else if ( query.type === undefined ) {
query.type = field;
}
}
if ( query.url === undefined ) { return; }
snfeQuery(lineNo, query);
});
/******************************************************************************/

View File

@@ -0,0 +1,288 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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';
// This module can be dynamically loaded or spun off as a worker.
/******************************************************************************/
const patches = new Map();
const encoder = new TextEncoder();
const reFileName = /([^\/]+?)(?:#.+)?$/;
const EMPTYLINE = '';
/******************************************************************************/
const suffleArray = arr => {
const out = arr.slice();
for ( let i = 0, n = out.length; i < n; i++ ) {
const j = Math.floor(Math.random() * n);
if ( j === i ) { continue; }
[ out[j], out[i] ] = [ out[i], out[j] ];
}
return out;
};
const basename = url => {
const match = reFileName.exec(url);
return match && match[1] || '';
};
const resolveURL = (path, url) => {
try {
return new URL(path, url);
}
catch(_) {
}
};
const expectedTimeFromPatch = assetDetails => {
const match = /(\d+)\.(\d+)\.(\d+)\.(\d+)/.exec(assetDetails.patchPath);
if ( match === null ) { return 0; }
const date = new Date();
date.setUTCFullYear(
parseInt(match[1], 10),
parseInt(match[2], 10) - 1,
parseInt(match[3], 10)
);
date.setUTCHours(0, parseInt(match[4], 10), 0, 0);
return date.getTime() + assetDetails.diffExpires;
};
function parsePatch(patch) {
const patchDetails = new Map();
const diffLines = patch.split('\n');
let i = 0, n = diffLines.length;
while ( i < n ) {
const line = diffLines[i++];
if ( line.startsWith('diff ') === false ) { continue; }
const fields = line.split(/\s+/);
const diffBlock = {};
for ( let j = 0; j < fields.length; j++ ) {
const field = fields[j];
const pos = field.indexOf(':');
if ( pos === -1 ) { continue; }
const name = field.slice(0, pos);
if ( name === '' ) { continue; }
const value = field.slice(pos+1);
switch ( name ) {
case 'name':
case 'checksum':
diffBlock[name] = value;
break;
case 'lines':
diffBlock.lines = parseInt(value, 10);
break;
default:
break;
}
}
if ( diffBlock.name === undefined ) { return; }
if ( isNaN(diffBlock.lines) || diffBlock.lines <= 0 ) { return; }
if ( diffBlock.checksum === undefined ) { return; }
patchDetails.set(diffBlock.name, diffBlock);
diffBlock.diff = diffLines.slice(i, i + diffBlock.lines).join('\n');
i += diffBlock.lines;
}
if ( patchDetails.size === 0 ) { return; }
return patchDetails;
}
function applyPatch(text, diff) {
// Inspired from (Perl) "sub _patch" at:
// https://twiki.org/p/pub/Codev/RcsLite/RcsLite.pm
// Apparently authored by John Talintyre in Jan. 2002
// https://twiki.org/cgi-bin/view/Codev/RcsLite
const lines = text.split('\n');
const diffLines = diff.split('\n');
let iAdjust = 0;
let iDiff = 0, nDiff = diffLines.length;
while ( iDiff < nDiff ) {
const diffLine = diffLines[iDiff++];
if ( diffLine === '' ) { break; }
const diffParsed = /^([ad])(\d+) (\d+)$/.exec(diffLine);
if ( diffParsed === null ) { return; }
const op = diffParsed[1];
const iOp = parseInt(diffParsed[2], 10);
const nOp = parseInt(diffParsed[3], 10);
const iOpAdj = iOp + iAdjust;
if ( iOpAdj > lines.length ) { return; }
// Delete lines
if ( op === 'd' ) {
lines.splice(iOpAdj-1, nOp);
iAdjust -= nOp;
continue;
}
// Add lines: Don't use splice() to avoid stack limit issues
for ( let i = 0; i < nOp; i++ ) {
lines.push(EMPTYLINE);
}
lines.copyWithin(iOpAdj+nOp, iOpAdj);
for ( let i = 0; i < nOp; i++ ) {
lines[iOpAdj+i] = diffLines[iDiff+i];
}
iAdjust += nOp;
iDiff += nOp;
}
return lines.join('\n');
}
function hasPatchDetails(assetDetails) {
const { patchPath } = assetDetails;
const patchFile = basename(patchPath);
return patchFile !== '' && patches.has(patchFile);
}
/******************************************************************************/
// Async
async function applyPatchAndValidate(assetDetails, diffDetails) {
const { text } = assetDetails;
const { diff, checksum } = diffDetails;
const textAfter = applyPatch(text, diff);
if ( typeof textAfter !== 'string' ) {
assetDetails.error = 'baddiff';
return false;
}
const crypto = globalThis.crypto;
if ( typeof crypto !== 'object' ) {
assetDetails.error = 'nocrypto';
return false;
}
const arrayin = encoder.encode(textAfter);
const arraybuffer = await crypto.subtle.digest('SHA-1', arrayin);
const arrayout = new Uint8Array(arraybuffer);
const sha1Full = Array.from(arrayout).map(i =>
i.toString(16).padStart(2, '0')
).join('');
if ( sha1Full.startsWith(checksum) === false ) {
assetDetails.error = `badchecksum: expected ${checksum}, computed ${sha1Full.slice(0, checksum.length)}`;
return false;
}
assetDetails.text = textAfter;
return true;
}
async function fetchPatchDetailsFromCDNs(assetDetails) {
const { patchPath, cdnURLs } = assetDetails;
if ( Array.isArray(cdnURLs) === false ) { return null; }
if ( cdnURLs.length === 0 ) { return null; }
for ( const cdnURL of suffleArray(cdnURLs) ) {
const patchURL = resolveURL(patchPath, cdnURL);
if ( patchURL === undefined ) { continue; }
const response = await fetch(patchURL).catch(reason => {
console.error(reason, patchURL);
});
if ( response === undefined ) { continue; }
if ( response.status === 404 ) { break; }
if ( response.ok !== true ) { continue; }
const patchText = await response.text();
const patchDetails = parsePatch(patchText);
if ( patchURL.hash.length > 1 ) {
assetDetails.diffName = patchURL.hash.slice(1);
patchURL.hash = '';
}
return {
patchURL: patchURL.href,
patchSize: `${(patchText.length / 1000).toFixed(1)} KB`,
patchDetails,
};
}
return null;
}
async function fetchPatchDetails(assetDetails) {
const { patchPath } = assetDetails;
const patchFile = basename(patchPath);
if ( patchFile === '' ) { return null; }
if ( patches.has(patchFile) ) {
return patches.get(patchFile);
}
const patchDetailsPromise = fetchPatchDetailsFromCDNs(assetDetails);
patches.set(patchFile, patchDetailsPromise);
return patchDetailsPromise;
}
async function fetchAndApplyAllPatches(assetDetails) {
if ( assetDetails.fetch === false ) {
if ( hasPatchDetails(assetDetails) === false ) {
assetDetails.status = 'nodiff';
return assetDetails;
}
}
// uBO-specific, to avoid pointless fetches which are likely to fail
// because the patch has not yet been created
const patchTime = expectedTimeFromPatch(assetDetails);
if ( patchTime > Date.now() ) {
assetDetails.status = 'nopatch-yet';
return assetDetails;
}
const patchData = await fetchPatchDetails(assetDetails);
if ( patchData === null ) {
assetDetails.status = (Date.now() - patchTime) < (4 * assetDetails.diffExpires)
? 'nopatch-yet'
: 'nopatch';
return assetDetails;
}
const { patchDetails } = patchData;
if ( patchDetails instanceof Map === false ) {
assetDetails.status = 'nodiff';
return assetDetails;
}
const diffDetails = patchDetails.get(assetDetails.diffName);
if ( diffDetails === undefined ) {
assetDetails.status = 'nodiff';
return assetDetails;
}
if ( assetDetails.text === undefined ) {
assetDetails.status = 'needtext';
return assetDetails;
}
const outcome = await applyPatchAndValidate(assetDetails, diffDetails);
if ( outcome !== true ) { return assetDetails; }
assetDetails.status = 'updated';
assetDetails.patchURL = patchData.patchURL;
assetDetails.patchSize = patchData.patchSize;
return assetDetails;
}
/******************************************************************************/
const bc = new globalThis.BroadcastChannel('diffUpdater');
bc.onmessage = ev => {
const message = ev.data || {};
switch ( message.what ) {
case 'update':
fetchAndApplyAllPatches(message).then(response => {
bc.postMessage(response);
}).catch(error => {
bc.postMessage({ what: 'broken', error });
});
break;
}
};
bc.postMessage({ what: 'ready' });
/******************************************************************************/

View File

@@ -0,0 +1,266 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2015-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
*/
import { dom, qs$ } from './dom.js';
import { i18n, i18n$ } from './i18n.js';
/******************************************************************************/
const messaging = vAPI.messaging;
let details = {};
{
const matches = /details=([^&]+)/.exec(window.location.search);
if ( matches !== null ) {
details = JSON.parse(decodeURIComponent(matches[1]));
}
}
/******************************************************************************/
(async ( ) => {
const response = await messaging.send('documentBlocked', {
what: 'listsFromNetFilter',
rawFilter: details.fs,
});
if ( response instanceof Object === false ) { return; }
let lists;
for ( const rawFilter in response ) {
if ( Object.prototype.hasOwnProperty.call(response, rawFilter) ) {
lists = response[rawFilter];
break;
}
}
if ( Array.isArray(lists) === false || lists.length === 0 ) {
qs$('#whyex').style.setProperty('visibility', 'collapse');
return;
}
const parent = qs$('#whyex > ul');
parent.firstElementChild.remove(); // remove placeholder element
for ( const list of lists ) {
const listElem = dom.clone('#templates .filterList');
const sourceElem = qs$(listElem, '.filterListSource');
sourceElem.href += encodeURIComponent(list.assetKey);
sourceElem.append(i18n.patchUnicodeFlags(list.title));
if ( typeof list.supportURL === 'string' && list.supportURL !== '' ) {
const supportElem = qs$(listElem, '.filterListSupport');
dom.attr(supportElem, 'href', list.supportURL);
dom.cl.remove(supportElem, 'hidden');
}
parent.appendChild(listElem);
}
qs$('#whyex').style.removeProperty('visibility');
})();
/******************************************************************************/
const urlToFragment = raw => {
try {
const fragment = new DocumentFragment();
const url = new URL(raw);
const hn = url.hostname;
const i = raw.indexOf(hn);
const b = document.createElement('b');
b.append(hn);
fragment.append(raw.slice(0,i), b, raw.slice(i+hn.length));
return fragment;
} catch(_) {
}
return raw;
};
/******************************************************************************/
dom.clear('#theURL > p > span:first-of-type');
qs$('#theURL > p > span:first-of-type').append(urlToFragment(details.url));
dom.text('#why', details.fs);
if ( typeof details.to === 'string' && details.to.length !== 0 ) {
const fragment = new DocumentFragment();
const text = i18n$('docblockedRedirectPrompt');
const linkPlaceholder = '{{url}}';
let pos = text.indexOf(linkPlaceholder);
if ( pos !== -1 ) {
const link = document.createElement('a');
link.href = details.to;
dom.cl.add(link, 'code');
link.append(urlToFragment(details.to));
fragment.append(
text.slice(0, pos),
link,
text.slice(pos + linkPlaceholder.length)
);
qs$('#urlskip').append(fragment);
dom.attr('#urlskip', 'hidden', null);
}
}
/******************************************************************************/
// https://github.com/gorhill/uBlock/issues/691
// Parse URL to extract as much useful information as possible. This is
// useful to assist the user in deciding whether to navigate to the web page.
(( ) => {
if ( typeof URL !== 'function' ) { return; }
const reURL = /^https?:\/\//;
const liFromParam = function(name, value) {
if ( value === '' ) {
value = name;
name = '';
}
const li = dom.create('li');
let span = dom.create('span');
dom.text(span, name);
li.appendChild(span);
if ( name !== '' && value !== '' ) {
li.appendChild(document.createTextNode(' = '));
}
span = dom.create('span');
if ( reURL.test(value) ) {
const a = dom.create('a');
dom.attr(a, 'href', value);
dom.text(a, value);
span.appendChild(a);
} else {
dom.text(span, value);
}
li.appendChild(span);
return li;
};
// https://github.com/uBlockOrigin/uBlock-issues/issues/1649
// Limit recursion.
const renderParams = function(parentNode, rawURL, depth = 0) {
let url;
try {
url = new URL(rawURL);
} catch(ex) {
return false;
}
const search = url.search.slice(1);
if ( search === '' ) { return false; }
url.search = '';
const li = liFromParam(i18n$('docblockedNoParamsPrompt'), url.href);
parentNode.appendChild(li);
const params = new self.URLSearchParams(search);
for ( const [ name, value ] of params ) {
const li = liFromParam(name, value);
if ( depth < 2 && reURL.test(value) ) {
const ul = dom.create('ul');
renderParams(ul, value, depth + 1);
li.appendChild(ul);
}
parentNode.appendChild(li);
}
return true;
};
if ( renderParams(qs$('#parsed'), details.url) === false ) {
return;
}
dom.cl.remove('#toggleParse', 'hidden');
dom.on('#toggleParse', 'click', ( ) => {
dom.cl.toggle('#theURL', 'collapsed');
vAPI.localStorage.setItem(
'document-blocked-expand-url',
(dom.cl.has('#theURL', 'collapsed') === false).toString()
);
});
vAPI.localStorage.getItemAsync('document-blocked-expand-url').then(value => {
dom.cl.toggle('#theURL', 'collapsed', value !== 'true' && value !== true);
});
})();
/******************************************************************************/
// https://www.reddit.com/r/uBlockOrigin/comments/breeux/close_this_window_doesnt_work_on_firefox/
if ( window.history.length > 1 ) {
dom.on('#back', 'click', ( ) => {
window.history.back();
});
qs$('#bye').style.display = 'none';
} else {
dom.on('#bye', 'click', ( ) => {
messaging.send('documentBlocked', {
what: 'closeThisTab',
});
});
qs$('#back').style.display = 'none';
}
/******************************************************************************/
const getTargetHostname = function() {
return details.hn;
};
const proceedToURL = function() {
window.location.replace(details.url);
};
const proceedTemporary = async function() {
await messaging.send('documentBlocked', {
what: 'temporarilyWhitelistDocument',
hostname: getTargetHostname(),
});
proceedToURL();
};
const proceedPermanent = async function() {
await messaging.send('documentBlocked', {
what: 'toggleHostnameSwitch',
name: 'no-strict-blocking',
hostname: getTargetHostname(),
deep: true,
state: true,
persist: true,
});
proceedToURL();
};
dom.on('#disableWarning', 'change', ev => {
const checked = ev.target.checked;
dom.cl.toggle('[data-i18n="docblockedBack"]', 'disabled', checked);
dom.cl.toggle('[data-i18n="docblockedClose"]', 'disabled', checked);
});
dom.on('#proceed', 'click', ( ) => {
if ( qs$('#disableWarning').checked ) {
proceedPermanent();
} else {
proceedTemporary();
}
});
/******************************************************************************/

View File

@@ -0,0 +1,68 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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';
/******************************************************************************/
/******************************************************************************/
const svgRoot = document.querySelector('svg');
let inspectorContentPort;
const shutdown = ( ) => {
inspectorContentPort.close();
inspectorContentPort.onmessage = inspectorContentPort.onmessageerror = null;
inspectorContentPort = undefined;
};
const contentInspectorChannel = ev => {
const msg = ev.data || {};
switch ( msg.what ) {
case 'quitInspector': {
shutdown();
break;
}
case 'svgPaths': {
const paths = svgRoot.children;
paths[0].setAttribute('d', msg.paths[0]);
paths[1].setAttribute('d', msg.paths[1]);
paths[2].setAttribute('d', msg.paths[2]);
paths[3].setAttribute('d', msg.paths[3]);
break;
}
default:
break;
}
};
// Wait for the content script to establish communication
globalThis.addEventListener('message', ev => {
const msg = ev.data || {};
if ( msg.what !== 'startInspector' ) { return; }
if ( Array.isArray(ev.ports) === false ) { return; }
if ( ev.ports.length === 0 ) { return; }
inspectorContentPort = ev.ports[0];
inspectorContentPort.onmessage = contentInspectorChannel;
inspectorContentPort.onmessageerror = shutdown;
inspectorContentPort.postMessage({ what: 'startInspector' });
}, { once: true });
/******************************************************************************/

217
uBlock0.chromium/js/dom.js Normal file
View File

@@ -0,0 +1,217 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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
*/
/******************************************************************************/
const normalizeTarget = target => {
if ( typeof target === 'string' ) { return Array.from(qsa$(target)); }
if ( target instanceof Element ) { return [ target ]; }
if ( target === null ) { return []; }
if ( Array.isArray(target) ) { return target; }
return Array.from(target);
};
const makeEventHandler = (selector, callback) => {
return function(event) {
const dispatcher = event.currentTarget;
if (
dispatcher instanceof HTMLElement === false ||
typeof dispatcher.querySelectorAll !== 'function'
) {
return;
}
const receiver = event.target;
const ancestor = receiver.closest(selector);
if (
ancestor === receiver &&
ancestor !== dispatcher &&
dispatcher.contains(ancestor)
) {
callback.call(receiver, event);
}
};
};
/******************************************************************************/
class dom {
static attr(target, attr, value = undefined) {
for ( const elem of normalizeTarget(target) ) {
if ( value === undefined ) {
return elem.getAttribute(attr);
}
if ( value === null ) {
elem.removeAttribute(attr);
} else {
elem.setAttribute(attr, value);
}
}
}
static clear(target) {
for ( const elem of normalizeTarget(target) ) {
while ( elem.firstChild !== null ) {
elem.removeChild(elem.firstChild);
}
}
}
static clone(target) {
const elements = normalizeTarget(target);
if ( elements.length === 0 ) { return null; }
return elements[0].cloneNode(true);
}
static create(a) {
if ( typeof a === 'string' ) {
return document.createElement(a);
}
}
static prop(target, prop, value = undefined) {
for ( const elem of normalizeTarget(target) ) {
if ( value === undefined ) { return elem[prop]; }
elem[prop] = value;
}
}
static text(target, text) {
const targets = normalizeTarget(target);
if ( text === undefined ) {
return targets.length !== 0 ? targets[0].textContent : undefined;
}
for ( const elem of targets ) {
elem.textContent = text;
}
}
static remove(target) {
for ( const elem of normalizeTarget(target) ) {
elem.remove();
}
}
static empty(target) {
for ( const elem of normalizeTarget(target) ) {
while ( elem.firstElementChild !== null ) {
elem.firstElementChild.remove();
}
}
}
// target, type, callback, [options]
// target, type, subtarget, callback, [options]
static on(target, type, subtarget, callback, options) {
if ( typeof subtarget === 'function' ) {
options = callback;
callback = subtarget;
subtarget = undefined;
if ( typeof options === 'boolean' ) {
options = { capture: true };
}
} else {
callback = makeEventHandler(subtarget, callback);
if ( options === undefined || typeof options === 'boolean' ) {
options = { capture: true };
} else {
options.capture = true;
}
}
const targets = target instanceof Window || target instanceof Document
? [ target ]
: normalizeTarget(target);
for ( const elem of targets ) {
elem.addEventListener(type, callback, options);
}
}
static off(target, type, callback, options) {
if ( typeof callback !== 'function' ) { return; }
if ( typeof options === 'boolean' ) {
options = { capture: true };
}
const targets = target instanceof Window || target instanceof Document
? [ target ]
: normalizeTarget(target);
for ( const elem of targets ) {
elem.removeEventListener(type, callback, options);
}
}
}
dom.cl = class {
static add(target, name) {
for ( const elem of normalizeTarget(target) ) {
elem.classList.add(name);
}
}
static remove(target, ...names) {
for ( const elem of normalizeTarget(target) ) {
elem.classList.remove(...names);
}
}
static toggle(target, name, state) {
let r;
for ( const elem of normalizeTarget(target) ) {
r = elem.classList.toggle(name, state);
}
return r;
}
static has(target, name) {
for ( const elem of normalizeTarget(target) ) {
if ( elem.classList.contains(name) ) {
return true;
}
}
return false;
}
};
/******************************************************************************/
function qs$(a, b) {
if ( typeof a === 'string') {
return document.querySelector(a);
}
if ( a === null ) { return null; }
return a.querySelector(b);
}
function qsa$(a, b) {
if ( typeof a === 'string') {
return document.querySelectorAll(a);
}
if ( a === null ) { return []; }
return a.querySelectorAll(b);
}
dom.root = qs$(':root');
dom.html = document.documentElement;
dom.head = document.head;
dom.body = document.body;
/******************************************************************************/
export { dom, qs$, qsa$ };

View File

@@ -0,0 +1,710 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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/uMatrix
*/
/* global CodeMirror, diff_match_patch, uBlockDashboard */
'use strict';
import publicSuffixList from '../lib/publicsuffixlist/publicsuffixlist.js';
import { hostnameFromURI } from './uri-utils.js';
import { i18n$ } from './i18n.js';
import { dom, qs$, qsa$ } from './dom.js';
import './codemirror/ubo-dynamic-filtering.js';
/******************************************************************************/
const hostnameToDomainMap = new Map();
const mergeView = new CodeMirror.MergeView(
qs$('.codeMirrorMergeContainer'),
{
allowEditingOriginals: true,
connect: 'align',
inputStyle: 'contenteditable',
lineNumbers: true,
lineWrapping: false,
origLeft: '',
revertButtons: true,
value: '',
}
);
mergeView.editor().setOption('styleActiveLine', true);
mergeView.editor().setOption('lineNumbers', false);
mergeView.leftOriginal().setOption('readOnly', 'nocursor');
uBlockDashboard.patchCodeMirrorEditor(mergeView.editor());
const thePanes = {
orig: {
doc: mergeView.leftOriginal(),
original: [],
modified: [],
},
edit: {
doc: mergeView.editor(),
original: [],
modified: [],
},
};
let cleanEditToken = 0;
let cleanEditText = '';
/******************************************************************************/
// The following code is to take care of properly internationalizing
// the tooltips of the arrows used by the CodeMirror merge view. These
// are hard-coded by CodeMirror ("Push to left", "Push to right"). An
// observer is necessary because there is no hook for uBO to overwrite
// reliably the default title attribute assigned by CodeMirror.
{
const i18nCommitStr = i18n$('rulesCommit');
const i18nRevertStr = i18n$('rulesRevert');
const commitArrowSelector = '.CodeMirror-merge-copybuttons-left .CodeMirror-merge-copy-reverse:not([title="' + i18nCommitStr + '"])';
const revertArrowSelector = '.CodeMirror-merge-copybuttons-left .CodeMirror-merge-copy:not([title="' + i18nRevertStr + '"])';
dom.attr('.CodeMirror-merge-scrolllock', 'title', i18n$('genericMergeViewScrollLock'));
const translate = function() {
let elems = qsa$(commitArrowSelector);
for ( const elem of elems ) {
dom.attr(elem, 'title', i18nCommitStr);
}
elems = qsa$(revertArrowSelector);
for ( const elem of elems ) {
dom.attr(elem, 'title', i18nRevertStr);
}
};
const mergeGapObserver = new MutationObserver(translate);
mergeGapObserver.observe(
qs$('.CodeMirror-merge-copybuttons-left'),
{ attributes: true, attributeFilter: [ 'title' ], subtree: true }
);
}
/******************************************************************************/
const getDiffer = (( ) => {
let differ;
return ( ) => {
if ( differ === undefined ) { differ = new diff_match_patch(); }
return differ;
};
})();
/******************************************************************************/
// Borrowed from...
// https://github.com/codemirror/CodeMirror/blob/3e1bb5fff682f8f6cbfaef0e56c61d62403d4798/addon/search/search.js#L22
// ... and modified as needed.
const updateOverlay = (( ) => {
let reFilter;
const mode = {
token: function(stream) {
if ( reFilter !== undefined ) {
reFilter.lastIndex = stream.pos;
let match = reFilter.exec(stream.string);
if ( match !== null ) {
if ( match.index === stream.pos ) {
stream.pos += match[0].length || 1;
return 'searching';
}
stream.pos = match.index;
return;
}
}
stream.skipToEnd();
}
};
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
function rulesToDoc(clearHistory) {
const orig = thePanes.orig.doc;
const edit = thePanes.edit.doc;
orig.startOperation();
edit.startOperation();
for ( const key in thePanes ) {
if ( thePanes.hasOwnProperty(key) === false ) { continue; }
const doc = thePanes[key].doc;
const rules = filterRules(key);
if (
clearHistory ||
doc.lineCount() === 1 && doc.getValue() === '' ||
rules.length === 0
) {
doc.setValue(rules.length !== 0 ? rules.join('\n') + '\n' : '');
continue;
}
// https://github.com/uBlockOrigin/uBlock-issues/issues/593
// Ensure the text content always ends with an empty line to avoid
// spurious diff entries.
// https://github.com/uBlockOrigin/uBlock-issues/issues/657
// Diff against unmodified beforeText so that the last newline can
// be reported in the diff and thus appended if needed.
let beforeText = doc.getValue();
let afterText = rules.join('\n').trim();
if ( afterText !== '' ) { afterText += '\n'; }
const diffs = getDiffer().diff_main(beforeText, afterText);
let i = diffs.length;
let iedit = beforeText.length;
while ( i-- ) {
const diff = diffs[i];
if ( diff[0] === 0 ) {
iedit -= diff[1].length;
continue;
}
const end = doc.posFromIndex(iedit);
if ( diff[0] === 1 ) {
doc.replaceRange(diff[1], end, end);
continue;
}
/* diff[0] === -1 */
iedit -= diff[1].length;
const beg = doc.posFromIndex(iedit);
doc.replaceRange('', beg, end);
}
}
// Mark ellipses as read-only
const marks = edit.getAllMarks();
for ( const mark of marks ) {
if ( mark.uboEllipsis !== true ) { continue; }
mark.clear();
}
if ( presentationState.isCollapsed ) {
for ( let iline = 0, n = edit.lineCount(); iline < n; iline++ ) {
if ( edit.getLine(iline) !== '...' ) { continue; }
const mark = edit.markText(
{ line: iline, ch: 0 },
{ line: iline + 1, ch: 0 },
{ atomic: true, readOnly: true }
);
mark.uboEllipsis = true;
}
}
orig.endOperation();
edit.endOperation();
cleanEditText = mergeView.editor().getValue().trim();
cleanEditToken = mergeView.editor().changeGeneration();
if ( clearHistory !== true ) { return; }
mergeView.editor().clearHistory();
const chunks = mergeView.leftChunks();
if ( chunks.length === 0 ) { return; }
const ldoc = thePanes.orig.doc;
const { clientHeight } = ldoc.getScrollInfo();
const line = Math.min(chunks[0].editFrom, chunks[0].origFrom);
ldoc.setCursor(line, 0);
ldoc.scrollIntoView(
{ line, ch: 0 },
(clientHeight - ldoc.defaultTextHeight()) / 2
);
}
/******************************************************************************/
function filterRules(key) {
const filter = qs$('#ruleFilter input').value;
const rules = thePanes[key].modified;
if ( filter === '' ) { return rules; }
const out = [];
for ( const rule of rules ) {
if ( rule.indexOf(filter) === -1 ) { continue; }
out.push(rule);
}
return out;
}
/******************************************************************************/
async function applyDiff(permanent, toAdd, toRemove) {
const details = await vAPI.messaging.send('dashboard', {
what: 'modifyRuleset',
permanent: permanent,
toAdd: toAdd,
toRemove: toRemove,
});
thePanes.orig.original = details.permanentRules;
thePanes.edit.original = details.sessionRules;
onPresentationChanged();
}
/******************************************************************************/
// CodeMirror quirk: sometimes fromStart.ch and/or toStart.ch is undefined.
// When this happens, use 0.
mergeView.options.revertChunk = function(
mv,
from, fromStart, fromEnd,
to, toStart, toEnd
) {
// https://github.com/gorhill/uBlock/issues/3611
if ( dom.attr(dom.body, 'dir') === 'rtl' ) {
let tmp = from; from = to; to = tmp;
tmp = fromStart; fromStart = toStart; toStart = tmp;
tmp = fromEnd; fromEnd = toEnd; toEnd = tmp;
}
if ( typeof fromStart.ch !== 'number' ) { fromStart.ch = 0; }
if ( fromEnd.ch !== 0 ) { fromEnd.line += 1; }
const toAdd = from.getRange(
{ line: fromStart.line, ch: 0 },
{ line: fromEnd.line, ch: 0 }
);
if ( typeof toStart.ch !== 'number' ) { toStart.ch = 0; }
if ( toEnd.ch !== 0 ) { toEnd.line += 1; }
const toRemove = to.getRange(
{ line: toStart.line, ch: 0 },
{ line: toEnd.line, ch: 0 }
);
applyDiff(from === mv.editor(), toAdd, toRemove);
};
/******************************************************************************/
function handleImportFilePicker() {
const fileReaderOnLoadHandler = function() {
if ( typeof this.result !== 'string' || this.result === '' ) { return; }
// https://github.com/chrisaljoudi/uBlock/issues/757
// Support RequestPolicy rule syntax
let result = this.result;
let matches = /\[origins-to-destinations\]([^\[]+)/.exec(result);
if ( matches && matches.length === 2 ) {
result = matches[1].trim()
.replace(/\|/g, ' ')
.replace(/\n/g, ' * noop\n');
}
applyDiff(false, result, '');
};
const file = this.files[0];
if ( file === undefined || file.name === '' ) { return; }
if ( file.type.indexOf('text') !== 0 ) { return; }
const fr = new FileReader();
fr.onload = fileReaderOnLoadHandler;
fr.readAsText(file);
}
/******************************************************************************/
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();
}
/******************************************************************************/
function exportUserRulesToFile() {
const filename = i18n$('rulesDefaultFileName')
.replace('{{datetime}}', uBlockDashboard.dateNowToSensibleString())
.replace(/ +/g, '_');
vAPI.download({
url: 'data:text/plain,' + encodeURIComponent(
mergeView.leftOriginal().getValue().trim() + '\n'
),
filename: filename,
saveAs: true
});
}
/******************************************************************************/
{
let timer;
dom.on('#ruleFilter input', 'input', ( ) => {
if ( timer !== undefined ) { self.cancelIdleCallback(timer); }
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 = (( ) => {
const reSwRule = /^([^/]+): ([^/ ]+) ([^ ]+)/;
const reRule = /^([^ ]+) ([^/ ]+) ([^ ]+ [^ ]+)/;
const reUrlRule = /^([^ ]+) ([^ ]+) ([^ ]+ [^ ]+)/;
const sortNormalizeHn = function(hn) {
let domain = hostnameToDomainMap.get(hn);
if ( domain === undefined ) {
domain = /(\d|\])$/.test(hn)
? hn
: publicSuffixList.getDomain(hn);
hostnameToDomainMap.set(hn, domain);
}
let normalized = domain || hn;
if ( hn.length !== domain.length ) {
const subdomains = hn.slice(0, hn.length - domain.length - 1);
normalized += '.' + (
subdomains.includes('.')
? subdomains.split('.').reverse().join('.')
: subdomains
);
}
return normalized;
};
const slotFromRule = rule => {
let type, srcHn, desHn, extra;
let match = reSwRule.exec(rule);
if ( match !== null ) {
type = ' ' + match[1];
srcHn = sortNormalizeHn(match[2]);
desHn = srcHn;
extra = match[3];
} else if ( (match = reRule.exec(rule)) !== null ) {
type = '\x10FFFE';
srcHn = sortNormalizeHn(match[1]);
desHn = sortNormalizeHn(match[2]);
extra = match[3];
} else if ( (match = reUrlRule.exec(rule)) !== null ) {
type = '\x10FFFF';
srcHn = sortNormalizeHn(match[1]);
desHn = sortNormalizeHn(hostnameFromURI(match[2]));
extra = match[3];
}
if ( presentationState.sortType === 0 ) {
return { rule, token: `${type} ${srcHn} ${desHn} ${extra}` };
}
if ( presentationState.sortType === 1 ) {
return { rule, token: `${srcHn} ${type} ${desHn} ${extra}` };
}
return { rule, token: `${desHn} ${type} ${srcHn} ${extra}` };
};
const sort = rules => {
const slots = [];
for ( let i = 0; i < rules.length; i++ ) {
slots.push(slotFromRule(rules[i], 1));
}
slots.sort((a, b) => a.token.localeCompare(b.token));
for ( let i = 0; i < rules.length; i++ ) {
rules[i] = slots[i].rule;
}
};
const collapse = ( ) => {
if ( presentationState.isCollapsed !== true ) { return; }
const diffs = getDiffer().diff_main(
thePanes.orig.modified.join('\n'),
thePanes.edit.modified.join('\n')
);
const ll = []; let il = 0, lellipsis = false;
const rr = []; let ir = 0, rellipsis = false;
for ( let i = 0; i < diffs.length; i++ ) {
const diff = diffs[i];
if ( diff[0] === 0 ) {
lellipsis = rellipsis = true;
il += 1; ir += 1;
continue;
}
if ( diff[0] < 0 ) {
if ( lellipsis ) {
ll.push('...');
if ( rellipsis ) { rr.push('...'); }
lellipsis = rellipsis = false;
}
ll.push(diff[1].trim());
il += 1;
continue;
}
/* diff[0] > 0 */
if ( rellipsis ) {
rr.push('...');
if ( lellipsis ) { ll.push('...'); }
lellipsis = rellipsis = false;
}
rr.push(diff[1].trim());
ir += 1;
}
if ( lellipsis ) { ll.push('...'); }
if ( rellipsis ) { rr.push('...'); }
thePanes.orig.modified = ll;
thePanes.edit.modified = rr;
};
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 mode = origPane.doc.getMode();
mode.sortType = presentationState.sortType;
mode.setHostnameToDomainMap(hostnameToDomainMap);
mode.setPSL(publicSuffixList);
}
{
const mode = editPane.doc.getMode();
mode.sortType = presentationState.sortType;
mode.setHostnameToDomainMap(hostnameToDomainMap);
mode.setPSL(publicSuffixList);
}
sort(origPane.modified);
sort(editPane.modified);
collapse();
rulesToDoc(clearHistory);
onTextChanged(clearHistory);
};
})();
/******************************************************************************/
const onTextChanged = (( ) => {
let timer;
const process = details => {
timer = undefined;
const diff = qs$('#diff');
let isClean = mergeView.editor().isClean(cleanEditToken);
if (
details === undefined &&
isClean === false &&
mergeView.editor().getValue().trim() === cleanEditText
) {
cleanEditToken = mergeView.editor().changeGeneration();
isClean = true;
}
const isDirty = mergeView.leftChunks().length !== 0;
dom.cl.toggle(dom.body, 'editing', isClean === false);
dom.cl.toggle(diff, 'dirty', isDirty);
dom.cl.toggle('#editSaveButton', 'disabled', isClean);
dom.cl.toggle('#exportButton,#importButton', 'disabled', isClean === false);
dom.cl.toggle('#revertButton,#commitButton', 'disabled', isClean === false || isDirty === false);
const input = qs$('#ruleFilter input');
if ( isClean ) {
dom.attr(input, 'disabled', null);
CodeMirror.commands.save = undefined;
} else {
dom.attr(input, 'disabled', '');
CodeMirror.commands.save = editSaveHandler;
}
};
return function onTextChanged(now) {
if ( timer !== undefined ) { self.cancelIdleCallback(timer); }
timer = now ? process() : self.requestIdleCallback(process, { timeout: 57 });
};
})();
/******************************************************************************/
function revertAllHandler() {
const toAdd = [], toRemove = [];
const left = mergeView.leftOriginal();
const edit = mergeView.editor();
for ( const chunk of mergeView.leftChunks() ) {
const addedLines = left.getRange(
{ line: chunk.origFrom, ch: 0 },
{ line: chunk.origTo, ch: 0 }
);
const removedLines = edit.getRange(
{ line: chunk.editFrom, ch: 0 },
{ line: chunk.editTo, ch: 0 }
);
toAdd.push(addedLines.trim());
toRemove.push(removedLines.trim());
}
applyDiff(false, toAdd.join('\n'), toRemove.join('\n'));
}
/******************************************************************************/
function commitAllHandler() {
const toAdd = [], toRemove = [];
const left = mergeView.leftOriginal();
const edit = mergeView.editor();
for ( const chunk of mergeView.leftChunks() ) {
const addedLines = edit.getRange(
{ line: chunk.editFrom, ch: 0 },
{ line: chunk.editTo, ch: 0 }
);
const removedLines = left.getRange(
{ line: chunk.origFrom, ch: 0 },
{ line: chunk.origTo, ch: 0 }
);
toAdd.push(addedLines.trim());
toRemove.push(removedLines.trim());
}
applyDiff(true, toAdd.join('\n'), toRemove.join('\n'));
}
/******************************************************************************/
function editSaveHandler() {
const editor = mergeView.editor();
const editText = editor.getValue().trim();
if ( editText === cleanEditText ) {
onTextChanged(true);
return;
}
const toAdd = [], toRemove = [];
const diffs = getDiffer().diff_main(cleanEditText, editText);
for ( const diff of diffs ) {
if ( diff[0] === 1 ) {
toAdd.push(diff[1]);
} else if ( diff[0] === -1 ) {
toRemove.push(diff[1]);
}
}
applyDiff(false, toAdd.join(''), toRemove.join(''));
}
/******************************************************************************/
self.cloud.onPush = function() {
return thePanes.orig.original.join('\n');
};
self.cloud.onPull = function(data, append) {
if ( typeof data !== 'string' ) { return; }
applyDiff(
false,
data,
append ? '' : mergeView.editor().getValue().trim()
);
};
/******************************************************************************/
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 => {
thePanes.orig.original = details.permanentRules;
thePanes.edit.original = details.sessionRules;
publicSuffixList.fromSelfie(details.pslSelfie);
onPresentationChanged(true);
});
// Handle user interaction
dom.on('#importButton', 'click', startImportFilePicker);
dom.on('#importFilePicker', 'change', handleImportFilePicker);
dom.on('#exportButton', 'click', exportUserRulesToFile);
dom.on('#revertButton', 'click', revertAllHandler);
dom.on('#commitButton', 'click', commitAllHandler);
dom.on('#editSaveButton', 'click', editSaveHandler);
// https://groups.google.com/forum/#!topic/codemirror/UQkTrt078Vs
mergeView.editor().on('updateDiff', ( ) => {
onTextChanged();
});
/******************************************************************************/

View File

@@ -0,0 +1,488 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-2018 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';
/******************************************************************************/
import punycode from '../lib/punycode.js';
import { LineIterator } from './text-utils.js';
import {
decomposeHostname,
domainFromHostname,
} from './uri-utils.js';
/******************************************************************************/
// Object.create(null) is used below to eliminate worries about unexpected
// property names in prototype chain -- and this way we don't have to use
// hasOwnProperty() to avoid this.
const supportedDynamicTypes = Object.create(null);
Object.assign(supportedDynamicTypes, {
'3p': true,
'image': true,
'inline-script': true,
'1p-script': true,
'3p-script': true,
'3p-frame': true
});
const typeBitOffsets = Object.create(null);
Object.assign(typeBitOffsets, {
'*': 0,
'inline-script': 2,
'1p-script': 4,
'3p-script': 6,
'3p-frame': 8,
'image': 10,
'3p': 12
});
const nameToActionMap = Object.create(null);
Object.assign(nameToActionMap, {
'block': 1,
'allow': 2,
'noop': 3
});
const intToActionMap = new Map([
[ 1, 'block' ],
[ 2, 'allow' ],
[ 3, 'noop' ]
]);
// For performance purpose, as simple tests as possible
const reBadHostname = /[^0-9a-z_.\[\]:%-]/;
const reNotASCII = /[^\x20-\x7F]/;
const decomposedSource = [];
const decomposedDestination = [];
/******************************************************************************/
function is3rdParty(srcHostname, desHostname) {
// If at least one is party-less, the relation can't be labelled
// "3rd-party"
if ( desHostname === '*' || srcHostname === '*' || srcHostname === '' ) {
return false;
}
// No domain can very well occurs, for examples:
// - localhost
// - file-scheme
// etc.
const srcDomain = domainFromHostname(srcHostname) || srcHostname;
if ( desHostname.endsWith(srcDomain) === false ) {
return true;
}
// Do not confuse 'example.com' with 'anotherexample.com'
return desHostname.length !== srcDomain.length &&
desHostname.charAt(desHostname.length - srcDomain.length - 1) !== '.';
}
/******************************************************************************/
class DynamicHostRuleFiltering {
constructor() {
this.reset();
}
reset() {
this.r = 0;
this.type = '';
this.y = '';
this.z = '';
this.rules = new Map();
this.changed = false;
}
assign(other) {
// Remove rules not in other
for ( const k of this.rules.keys() ) {
if ( other.rules.has(k) === false ) {
this.rules.delete(k);
this.changed = true;
}
}
// Add/change rules in other
for ( const entry of other.rules ) {
if ( this.rules.get(entry[0]) !== entry[1] ) {
this.rules.set(entry[0], entry[1]);
this.changed = true;
}
}
}
copyRules(from, srcHostname, desHostnames) {
// Specific types
let thisBits = this.rules.get('* *');
let fromBits = from.rules.get('* *');
if ( fromBits !== thisBits ) {
if ( fromBits !== undefined ) {
this.rules.set('* *', fromBits);
} else {
this.rules.delete('* *');
}
this.changed = true;
}
let key = `${srcHostname} *`;
thisBits = this.rules.get(key);
fromBits = from.rules.get(key);
if ( fromBits !== thisBits ) {
if ( fromBits !== undefined ) {
this.rules.set(key, fromBits);
} else {
this.rules.delete(key);
}
this.changed = true;
}
// Specific destinations
for ( const desHostname in desHostnames ) {
key = `* ${desHostname}`;
thisBits = this.rules.get(key);
fromBits = from.rules.get(key);
if ( fromBits !== thisBits ) {
if ( fromBits !== undefined ) {
this.rules.set(key, fromBits);
} else {
this.rules.delete(key);
}
this.changed = true;
}
key = `${srcHostname} ${desHostname}` ;
thisBits = this.rules.get(key);
fromBits = from.rules.get(key);
if ( fromBits !== thisBits ) {
if ( fromBits !== undefined ) {
this.rules.set(key, fromBits);
} else {
this.rules.delete(key);
}
this.changed = true;
}
}
return this.changed;
}
// - * * type
// - from * type
// - * to *
// - from to *
hasSameRules(other, srcHostname, desHostnames) {
// Specific types
let key = '* *';
if ( this.rules.get(key) !== other.rules.get(key) ) { return false; }
key = `${srcHostname} *`;
if ( this.rules.get(key) !== other.rules.get(key) ) { return false; }
// Specific destinations
for ( const desHostname in desHostnames ) {
key = `* ${desHostname}`;
if ( this.rules.get(key) !== other.rules.get(key) ) {
return false;
}
key = `${srcHostname} ${desHostname}`;
if ( this.rules.get(key) !== other.rules.get(key) ) {
return false;
}
}
return true;
}
setCell(srcHostname, desHostname, type, state) {
const bitOffset = typeBitOffsets[type];
const k = `${srcHostname} ${desHostname}`;
const oldBitmap = this.rules.get(k) || 0;
const newBitmap = oldBitmap & ~(3 << bitOffset) | (state << bitOffset);
if ( newBitmap === oldBitmap ) { return false; }
if ( newBitmap === 0 ) {
this.rules.delete(k);
} else {
this.rules.set(k, newBitmap);
}
this.changed = true;
return true;
}
unsetCell(srcHostname, desHostname, type) {
this.evaluateCellZY(srcHostname, desHostname, type);
if ( this.r === 0 ) { return false; }
this.setCell(srcHostname, desHostname, type, 0);
this.changed = true;
return true;
}
evaluateCell(srcHostname, desHostname, type) {
const key = `${srcHostname} ${desHostname}`;
const bitmap = this.rules.get(key);
if ( bitmap === undefined ) { return 0; }
return bitmap >> typeBitOffsets[type] & 3;
}
clearRegisters() {
this.r = 0;
this.type = this.y = this.z = '';
return this;
}
evaluateCellZ(srcHostname, desHostname, type) {
decomposeHostname(srcHostname, decomposedSource);
this.type = type;
const bitOffset = typeBitOffsets[type];
for ( const srchn of decomposedSource ) {
this.z = srchn;
let v = this.rules.get(`${srchn} ${desHostname}`);
if ( v === undefined ) { continue; }
v = v >>> bitOffset & 3;
if ( v === 0 ) { continue; }
return (this.r = v);
}
// srcHostname is '*' at this point
this.r = 0;
return 0;
}
evaluateCellZY(srcHostname, desHostname, type) {
// Pathological cases.
if ( desHostname === '' ) {
this.r = 0;
return 0;
}
// Precedence: from most specific to least specific
// Specific-destination, any party, any type
decomposeHostname(desHostname, decomposedDestination);
for ( const deshn of decomposedDestination ) {
if ( deshn === '*' ) { break; }
this.y = deshn;
if ( this.evaluateCellZ(srcHostname, deshn, '*') !== 0 ) {
return this.r;
}
}
const thirdParty = is3rdParty(srcHostname, desHostname);
// Any destination
this.y = '*';
// Specific party
if ( thirdParty ) {
// 3rd-party, specific type
if ( type === 'script' ) {
if ( this.evaluateCellZ(srcHostname, '*', '3p-script') !== 0 ) {
return this.r;
}
} else if ( type === 'sub_frame' || type === 'object' ) {
if ( this.evaluateCellZ(srcHostname, '*', '3p-frame') !== 0 ) {
return this.r;
}
}
// 3rd-party, any type
if ( this.evaluateCellZ(srcHostname, '*', '3p') !== 0 ) {
return this.r;
}
} else if ( type === 'script' ) {
// 1st party, specific type
if ( this.evaluateCellZ(srcHostname, '*', '1p-script') !== 0 ) {
return this.r;
}
}
// Any destination, any party, specific type
if ( supportedDynamicTypes[type] !== undefined ) {
if ( this.evaluateCellZ(srcHostname, '*', type) !== 0 ) {
return this.r;
}
if ( type.startsWith('3p-') ) {
if ( this.evaluateCellZ(srcHostname, '*', '3p') !== 0 ) {
return this.r;
}
}
}
// Any destination, any party, any type
if ( this.evaluateCellZ(srcHostname, '*', '*') !== 0 ) {
return this.r;
}
this.type = '';
return 0;
}
mustAllowCellZY(srcHostname, desHostname, type) {
return this.evaluateCellZY(srcHostname, desHostname, type) === 2;
}
mustBlockOrAllow() {
return this.r === 1 || this.r === 2;
}
mustBlock() {
return this.r === 1;
}
mustAbort() {
return this.r === 3;
}
lookupRuleData(src, des, type) {
const r = this.evaluateCellZY(src, des, type);
if ( r === 0 ) { return; }
return `${this.z} ${this.y} ${this.type} ${r}`;
}
toLogData() {
if ( this.r === 0 || this.type === '' ) { return; }
return {
source: 'dynamicHost',
result: this.r,
raw: `${this.z} ${this.y} ${this.type} ${intToActionMap.get(this.r)}`
};
}
srcHostnameFromRule(rule) {
return rule.slice(0, rule.indexOf(' '));
}
desHostnameFromRule(rule) {
return rule.slice(rule.indexOf(' ') + 1);
}
toArray() {
const out = [];
for ( const key of this.rules.keys() ) {
const srchn = this.srcHostnameFromRule(key);
const deshn = this.desHostnameFromRule(key);
const srchnPretty = srchn.includes('xn--') && punycode
? punycode.toUnicode(srchn)
: srchn;
const deshnPretty = deshn.includes('xn--') && punycode
? punycode.toUnicode(deshn)
: deshn;
for ( const type in typeBitOffsets ) {
if ( typeBitOffsets[type] === undefined ) { continue; }
const val = this.evaluateCell(srchn, deshn, type);
if ( val === 0 ) { continue; }
const action = intToActionMap.get(val);
if ( action === undefined ) { continue; }
out.push(`${srchnPretty} ${deshnPretty} ${type} ${action}`);
}
}
return out;
}
toString() {
return this.toArray().join('\n');
}
fromString(text, append) {
const lineIter = new LineIterator(text);
if ( append !== true ) { this.reset(); }
while ( lineIter.eot() === false ) {
this.addFromRuleParts(lineIter.next().trim().split(/\s+/));
}
}
validateRuleParts(parts) {
if ( parts.length < 4 ) { return; }
// Ignore hostname-based switch rules
if ( parts[0].endsWith(':') ) { return; }
// Ignore URL-based rules
if ( parts[1].includes('/') ) { return; }
if ( typeBitOffsets[parts[2]] === undefined ) { return; }
if ( nameToActionMap[parts[3]] === undefined ) { return; }
// https://github.com/chrisaljoudi/uBlock/issues/840
// Discard invalid rules
if ( parts[1] !== '*' && parts[2] !== '*' ) { return; }
// Performance: avoid punycoding when only ASCII chars
if ( punycode !== undefined ) {
if ( reNotASCII.test(parts[0]) ) {
parts[0] = punycode.toASCII(parts[0]);
}
if ( reNotASCII.test(parts[1]) ) {
parts[1] = punycode.toASCII(parts[1]);
}
}
// https://github.com/chrisaljoudi/uBlock/issues/1082
// Discard rules with invalid hostnames
if (
(parts[0] !== '*' && reBadHostname.test(parts[0])) ||
(parts[1] !== '*' && reBadHostname.test(parts[1]))
) {
return;
}
return parts;
}
addFromRuleParts(parts) {
if ( this.validateRuleParts(parts) !== undefined ) {
this.setCell(parts[0], parts[1], parts[2], nameToActionMap[parts[3]]);
return true;
}
return false;
}
removeFromRuleParts(parts) {
if ( this.validateRuleParts(parts) !== undefined ) {
this.setCell(parts[0], parts[1], parts[2], 0);
return true;
}
return false;
}
toSelfie() {
return {
magicId: this.magicId,
rules: Array.from(this.rules)
};
}
fromSelfie(selfie) {
if ( selfie.magicId !== this.magicId ) { return false; }
this.rules = new Map(selfie.rules);
this.changed = true;
return true;
}
}
DynamicHostRuleFiltering.prototype.magicId = 1;
/******************************************************************************/
export default DynamicHostRuleFiltering;
/******************************************************************************/

View File

@@ -0,0 +1,917 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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
*/
/* global CodeMirror */
import './codemirror/ubo-static-filtering.js';
import * as sfp from './static-filtering-parser.js';
import { dom } from './dom.js';
import { hostnameFromURI } from './uri-utils.js';
import punycode from '../lib/punycode.js';
/******************************************************************************/
/******************************************************************************/
(( ) => {
/******************************************************************************/
if ( typeof vAPI !== 'object' ) { return; }
const $id = id => document.getElementById(id);
const $stor = selector => document.querySelector(selector);
const $storAll = selector => document.querySelectorAll(selector);
const pickerRoot = document.documentElement;
const dialog = $stor('aside');
let staticFilteringParser;
const svgRoot = $stor('svg#sea');
const svgOcean = svgRoot.children[0];
const svgIslands = svgRoot.children[1];
const NoPaths = 'M0 0';
const reCosmeticAnchor = /^#(\$|\?|\$\?)?#/;
{
const url = new URL(self.location.href);
if ( url.searchParams.has('zap') ) {
pickerRoot.classList.add('zap');
}
}
const docURL = new URL(vAPI.getURL(''));
const computedSpecificityCandidates = new Map();
let resultsetOpt;
let cosmeticFilterCandidates = [];
let computedCandidate = '';
let needBody = false;
/******************************************************************************/
const cmEditor = new CodeMirror(document.querySelector('.codeMirrorContainer'), {
autoCloseBrackets: true,
autofocus: true,
extraKeys: {
'Ctrl-Space': 'autocomplete',
},
lineWrapping: true,
matchBrackets: true,
maxScanLines: 1,
});
vAPI.messaging.send('dashboard', {
what: 'getAutoCompleteDetails'
}).then(hints => {
// For unknown reasons, `instanceof Object` does not work here in Firefox.
if ( hints instanceof Object === false ) { return; }
cmEditor.setOption('uboHints', hints);
});
/******************************************************************************/
const rawFilterFromTextarea = function() {
const text = cmEditor.getValue();
const pos = text.indexOf('\n');
return pos === -1 ? text : text.slice(0, pos);
};
/******************************************************************************/
const filterFromTextarea = function() {
const filter = rawFilterFromTextarea();
if ( filter === '' ) { return ''; }
const parser = staticFilteringParser;
parser.parse(filter);
if ( parser.isFilter() === false ) { return '!'; }
if ( parser.isExtendedFilter() ) {
if ( parser.isCosmeticFilter() === false ) { return '!'; }
} else if ( parser.isNetworkFilter() === false ) {
return '!';
}
return filter;
};
/******************************************************************************/
const renderRange = function(id, value, invert = false) {
const input = $stor(`#${id} input`);
const max = parseInt(input.max, 10);
if ( typeof value !== 'number' ) {
value = parseInt(input.value, 10);
}
if ( invert ) {
value = max - value;
}
input.value = value;
const slider = $stor(`#${id} > span`);
const lside = slider.children[0];
const thumb = slider.children[1];
const sliderWidth = slider.offsetWidth;
const maxPercent = (sliderWidth - thumb.offsetWidth) / sliderWidth * 100;
const widthPercent = value / max * maxPercent;
lside.style.width = `${widthPercent}%`;
};
/******************************************************************************/
const userFilterFromCandidate = function(filter) {
if ( filter === '' || filter === '!' ) { return; }
let hn = hostnameFromURI(docURL.href);
if ( hn.startsWith('xn--') ) {
hn = punycode.toUnicode(hn);
}
// Cosmetic filter?
if ( reCosmeticAnchor.test(filter) ) {
return hn + filter;
}
// Assume net filter
const opts = [];
// If no domain included in filter, we need domain option
if ( filter.startsWith('||') === false ) {
opts.push(`domain=${hn}`);
}
if ( resultsetOpt !== undefined ) {
opts.push(resultsetOpt);
}
if ( opts.length ) {
filter += '$' + opts.join(',');
}
return filter;
};
/******************************************************************************/
const candidateFromFilterChoice = function(filterChoice) {
let { slot, filters } = filterChoice;
let filter = filters[slot];
// https://github.com/uBlockOrigin/uBlock-issues/issues/47
for ( const elem of $storAll('#candidateFilters li') ) {
elem.classList.remove('active');
}
computedCandidate = '';
if ( filter === undefined ) { return ''; }
// For net filters there no such thing as a path
if ( filter.startsWith('##') === false ) {
$stor(`#netFilters li:nth-of-type(${slot+1})`)
.classList.add('active');
return filter;
}
// At this point, we have a cosmetic filter
$stor(`#cosmeticFilters li:nth-of-type(${slot+1})`)
.classList.add('active');
return cosmeticCandidatesFromFilterChoice(filterChoice);
};
/******************************************************************************/
const cosmeticCandidatesFromFilterChoice = function(filterChoice) {
let { slot, filters } = filterChoice;
renderRange('resultsetDepth', slot, true);
renderRange('resultsetSpecificity');
if ( computedSpecificityCandidates.has(slot) ) {
onCandidatesOptimized({ slot });
return;
}
const specificities = [
0b0000, // remove hierarchy; remove id, nth-of-type, attribute values
0b0010, // remove hierarchy; remove id, nth-of-type
0b0011, // remove hierarchy
0b1000, // trim hierarchy; remove id, nth-of-type, attribute values
0b1010, // trim hierarchy; remove id, nth-of-type
0b1100, // remove id, nth-of-type, attribute values
0b1110, // remove id, nth-of-type
0b1111, // keep all = most specific
];
const candidates = [];
let filter = filters[slot];
for ( const specificity of specificities ) {
// Return path: the target element, then all siblings prepended
const paths = [];
for ( let i = slot; i < filters.length; i++ ) {
filter = filters[i].slice(2);
// Remove id, nth-of-type
// https://github.com/uBlockOrigin/uBlock-issues/issues/162
// Mind escaped periods: they do not denote a class identifier.
if ( (specificity & 0b0001) === 0 ) {
filter = filter.replace(/:nth-of-type\(\d+\)/, '');
if (
filter.charAt(0) === '#' && (
(specificity & 0b1000) === 0 || i === slot
)
) {
const pos = filter.search(/[^\\]\./);
if ( pos !== -1 ) {
filter = filter.slice(pos + 1);
}
}
}
// Remove attribute values.
if ( (specificity & 0b0010) === 0 ) {
const match = /^\[([^^*$=]+)[\^*$]?=.+\]$/.exec(filter);
if ( match !== null ) {
filter = `[${match[1]}]`;
}
}
// Remove all classes when an id exists.
// https://github.com/uBlockOrigin/uBlock-issues/issues/162
// Mind escaped periods: they do not denote a class identifier.
if ( filter.charAt(0) === '#' ) {
filter = filter.replace(/([^\\])\..+$/, '$1');
}
if ( paths.length !== 0 ) {
filter += ' > ';
}
paths.unshift(filter);
// Stop at any element with an id: these are unique in a web page
if ( (specificity & 0b1000) === 0 || filter.startsWith('#') ) {
break;
}
}
// Trim hierarchy: remove generic elements from path
if ( (specificity & 0b1100) === 0b1000 ) {
let i = 0;
while ( i < paths.length - 1 ) {
if ( /^[a-z0-9]+ > $/.test(paths[i+1]) ) {
if ( paths[i].endsWith(' > ') ) {
paths[i] = paths[i].slice(0, -2);
}
paths.splice(i + 1, 1);
} else {
i += 1;
}
}
}
if (
needBody &&
paths.length !== 0 &&
paths[0].startsWith('#') === false &&
paths[0].startsWith('body ') === false &&
(specificity & 0b1100) !== 0
) {
paths.unshift('body > ');
}
candidates.push(paths);
}
pickerContentPort.postMessage({
what: 'optimizeCandidates',
candidates,
slot,
});
};
/******************************************************************************/
const onCandidatesOptimized = function(details) {
$id('resultsetModifiers').classList.remove('hide');
const i = parseInt($stor('#resultsetSpecificity input').value, 10);
if ( Array.isArray(details.candidates) ) {
computedSpecificityCandidates.set(details.slot, details.candidates);
}
const candidates = computedSpecificityCandidates.get(details.slot);
computedCandidate = candidates[i];
cmEditor.setValue(computedCandidate);
cmEditor.clearHistory();
onCandidateChanged();
};
/******************************************************************************/
const onSvgClicked = function(ev) {
// If zap mode, highlight element under mouse, this makes the zapper usable
// on touch screens.
if ( pickerRoot.classList.contains('zap') ) {
pickerContentPort.postMessage({
what: 'zapElementAtPoint',
mx: ev.clientX,
my: ev.clientY,
options: {
stay: ev.shiftKey || ev.type === 'touch',
highlight: ev.target !== svgIslands,
},
});
return;
}
// https://github.com/chrisaljoudi/uBlock/issues/810#issuecomment-74600694
// Unpause picker if:
// - click outside dialog AND
// - not in preview mode
if ( pickerRoot.classList.contains('paused') ) {
if ( pickerRoot.classList.contains('preview') === false ) {
unpausePicker();
}
return;
}
// Force dialog to always be visible when using a touch-driven device.
if ( ev.type === 'touch' ) {
pickerRoot.classList.add('show');
}
pickerContentPort.postMessage({
what: 'filterElementAtPoint',
mx: ev.clientX,
my: ev.clientY,
broad: ev.ctrlKey,
});
};
/*******************************************************************************
Swipe right:
If picker not paused: quit picker
If picker paused and dialog visible: hide dialog
If picker paused and dialog not visible: quit picker
Swipe left:
If picker paused and dialog not visible: show dialog
*/
const onSvgTouch = (( ) => {
let startX = 0, startY = 0;
let t0 = 0;
return ev => {
if ( ev.type === 'touchstart' ) {
startX = ev.touches[0].screenX;
startY = ev.touches[0].screenY;
t0 = ev.timeStamp;
return;
}
if ( startX === undefined ) { return; }
const stopX = ev.changedTouches[0].screenX;
const stopY = ev.changedTouches[0].screenY;
const angle = Math.abs(Math.atan2(stopY - startY, stopX - startX));
const distance = Math.sqrt(
Math.pow(stopX - startX, 2) +
Math.pow(stopY - startY, 2)
);
// Interpret touch events as a tap if:
// - Swipe is not valid; and
// - The time between start and stop was less than 200ms.
const duration = ev.timeStamp - t0;
if ( distance < 32 && duration < 200 ) {
onSvgClicked({
type: 'touch',
target: ev.target,
clientX: ev.changedTouches[0].pageX,
clientY: ev.changedTouches[0].pageY,
});
ev.preventDefault();
return;
}
if ( distance < 64 ) { return; }
const angleUpperBound = Math.PI * 0.25 * 0.5;
const swipeRight = angle < angleUpperBound;
if ( swipeRight === false && angle < Math.PI - angleUpperBound ) {
return;
}
if ( ev.cancelable ) {
ev.preventDefault();
}
// Swipe left.
if ( swipeRight === false ) {
if ( pickerRoot.classList.contains('paused') ) {
pickerRoot.classList.remove('hide');
pickerRoot.classList.add('show');
}
return;
}
// Swipe right.
if (
pickerRoot.classList.contains('zap') &&
svgIslands.getAttribute('d') !== NoPaths
) {
pickerContentPort.postMessage({
what: 'unhighlight'
});
return;
}
else if (
pickerRoot.classList.contains('paused') &&
pickerRoot.classList.contains('show')
) {
pickerRoot.classList.remove('show');
pickerRoot.classList.add('hide');
return;
}
quitPicker();
};
})();
/******************************************************************************/
const onCandidateChanged = function() {
const filter = filterFromTextarea();
const bad = filter === '!';
$stor('section').classList.toggle('invalidFilter', bad);
if ( bad ) {
$id('resultsetCount').textContent = 'E';
$id('create').setAttribute('disabled', '');
}
const text = rawFilterFromTextarea();
$id('resultsetModifiers').classList.toggle(
'hide', text === '' || text !== computedCandidate
);
pickerContentPort.postMessage({
what: 'dialogSetFilter',
filter,
compiled: reCosmeticAnchor.test(filter)
? staticFilteringParser.result.compiled
: undefined,
});
};
/******************************************************************************/
const onPreviewClicked = function() {
const state = pickerRoot.classList.toggle('preview');
pickerContentPort.postMessage({
what: 'togglePreview',
state,
});
};
/******************************************************************************/
const onCreateClicked = function() {
const candidate = filterFromTextarea();
const filter = userFilterFromCandidate(candidate);
if ( filter !== undefined ) {
vAPI.messaging.send('elementPicker', {
what: 'createUserFilter',
autoComment: true,
filters: filter,
docURL: docURL.href,
killCache: reCosmeticAnchor.test(candidate) === false,
});
}
pickerContentPort.postMessage({
what: 'dialogCreate',
filter: candidate,
compiled: reCosmeticAnchor.test(candidate)
? staticFilteringParser.result.compiled
: undefined,
});
};
/******************************************************************************/
const onPickClicked = function() {
unpausePicker();
};
/******************************************************************************/
const onQuitClicked = function() {
quitPicker();
};
/******************************************************************************/
const onDepthChanged = function() {
const input = $stor('#resultsetDepth input');
const max = parseInt(input.max, 10);
const value = parseInt(input.value, 10);
const text = candidateFromFilterChoice({
filters: cosmeticFilterCandidates,
slot: max - value,
});
if ( text === undefined ) { return; }
cmEditor.setValue(text);
cmEditor.clearHistory();
onCandidateChanged();
};
/******************************************************************************/
const onSpecificityChanged = function() {
renderRange('resultsetSpecificity');
if ( rawFilterFromTextarea() !== computedCandidate ) { return; }
const depthInput = $stor('#resultsetDepth input');
const slot = parseInt(depthInput.max, 10) - parseInt(depthInput.value, 10);
const i = parseInt($stor('#resultsetSpecificity input').value, 10);
const candidates = computedSpecificityCandidates.get(slot);
computedCandidate = candidates[i];
cmEditor.setValue(computedCandidate);
cmEditor.clearHistory();
onCandidateChanged();
};
/******************************************************************************/
const onCandidateClicked = function(ev) {
let li = ev.target.closest('li');
if ( li === null ) { return; }
const ul = li.closest('.changeFilter');
if ( ul === null ) { return; }
const choice = {
filters: Array.from(ul.querySelectorAll('li')).map(a => a.textContent),
slot: 0,
};
while ( li.previousElementSibling !== null ) {
li = li.previousElementSibling;
choice.slot += 1;
}
const text = candidateFromFilterChoice(choice);
if ( text === undefined ) { return; }
cmEditor.setValue(text);
cmEditor.clearHistory();
onCandidateChanged();
};
/******************************************************************************/
const onKeyPressed = function(ev) {
// Delete
if (
(ev.key === 'Delete' || ev.key === 'Backspace') &&
pickerRoot.classList.contains('zap')
) {
pickerContentPort.postMessage({
what: 'zapElementAtPoint',
options: { stay: true },
});
return;
}
// Esc
if ( ev.key === 'Escape' || ev.which === 27 ) {
onQuitClicked();
return;
}
};
/******************************************************************************/
const onStartMoving = (( ) => {
let isTouch = false;
let mx0 = 0, my0 = 0;
let mx1 = 0, my1 = 0;
let pw = 0, ph = 0;
let dw = 0, dh = 0;
let cx0 = 0, cy0 = 0;
let timer;
const eatEvent = function(ev) {
ev.stopPropagation();
ev.preventDefault();
};
const move = ( ) => {
timer = undefined;
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 => {
if ( timer !== undefined ) { return; }
if ( isTouch ) {
const touch = ev.touches[0];
mx1 = touch.pageX;
my1 = touch.pageY;
} else {
mx1 = ev.pageX;
my1 = ev.pageY;
}
timer = self.requestAnimationFrame(move);
};
const stop = ev => {
if ( dialog.classList.contains('moving') === false ) { return; }
dialog.classList.remove('moving');
if ( isTouch ) {
self.removeEventListener('touchmove', moveAsync, { capture: true });
} else {
self.removeEventListener('mousemove', moveAsync, { capture: true });
}
eatEvent(ev);
};
return ev => {
const target = dialog.querySelector('#move');
if ( ev.target !== target ) { return; }
if ( dialog.classList.contains('moving') ) { return; }
isTouch = ev.type.startsWith('touch');
if ( isTouch ) {
const touch = ev.touches[0];
mx0 = touch.pageX;
my0 = touch.pageY;
} else {
mx0 = ev.pageX;
my0 = ev.pageY;
}
const rect = dialog.getBoundingClientRect();
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 });
self.addEventListener('touchend', stop, { capture: true, once: true });
} else {
self.addEventListener('mousemove', moveAsync, { capture: true });
self.addEventListener('mouseup', stop, { capture: true, once: true });
}
eatEvent(ev);
};
})();
/******************************************************************************/
const svgListening = (( ) => {
let on = false;
let timer;
let mx = 0, my = 0;
const onTimer = ( ) => {
timer = undefined;
pickerContentPort.postMessage({
what: 'highlightElementAtPoint',
mx,
my,
});
};
const onHover = ev => {
mx = ev.clientX;
my = ev.clientY;
if ( timer === undefined ) {
timer = self.requestAnimationFrame(onTimer);
}
};
return state => {
if ( state === on ) { return; }
on = state;
if ( on ) {
document.addEventListener('mousemove', onHover, { passive: true });
return;
}
document.removeEventListener('mousemove', onHover, { passive: true });
if ( timer !== undefined ) {
self.cancelAnimationFrame(timer);
timer = undefined;
}
};
})();
/******************************************************************************/
// Create lists of candidate filters. This takes into account whether the
// current mode is narrow or broad.
const populateCandidates = function(candidates, selector) {
const root = dialog.querySelector(selector);
const ul = root.querySelector('ul');
while ( ul.firstChild !== null ) {
ul.firstChild.remove();
}
for ( let i = 0; i < candidates.length; i++ ) {
const li = document.createElement('li');
li.textContent = candidates[i];
ul.appendChild(li);
}
if ( candidates.length !== 0 ) {
root.style.removeProperty('display');
} else {
root.style.setProperty('display', 'none');
}
};
/******************************************************************************/
const showDialog = function(details) {
pausePicker();
const { netFilters, cosmeticFilters, filter } = details;
needBody =
cosmeticFilters.length !== 0 &&
cosmeticFilters[cosmeticFilters.length - 1] === '##body';
if ( needBody ) {
cosmeticFilters.pop();
}
cosmeticFilterCandidates = cosmeticFilters;
docURL.href = details.url;
populateCandidates(netFilters, '#netFilters');
populateCandidates(cosmeticFilters, '#cosmeticFilters');
computedSpecificityCandidates.clear();
const depthInput = $stor('#resultsetDepth input');
depthInput.max = cosmeticFilters.length - 1;
depthInput.value = depthInput.max;
dialog.querySelector('ul').style.display =
netFilters.length || cosmeticFilters.length ? '' : 'none';
$id('create').setAttribute('disabled', '');
// Auto-select a candidate filter
// 2020-09-01:
// In Firefox, `details instanceof Object` resolves to `false` despite
// `details` being a valid object. Consequently, falling back to use
// `typeof details`.
// This is an issue which surfaced when the element picker code was
// revisited to isolate the picker dialog DOM from the page DOM.
if ( typeof filter !== 'object' || filter === null ) {
cmEditor.setValue('');
return;
}
const filterChoice = {
filters: filter.filters,
slot: filter.slot,
};
const text = candidateFromFilterChoice(filterChoice);
if ( text === undefined ) { return; }
cmEditor.setValue(text);
onCandidateChanged();
};
/******************************************************************************/
const pausePicker = function() {
dom.cl.add(pickerRoot, 'paused');
dom.cl.remove(pickerRoot, 'minimized');
svgListening(false);
};
/******************************************************************************/
const unpausePicker = function() {
dom.cl.remove(pickerRoot, 'paused', 'preview');
dom.cl.add(pickerRoot, 'minimized');
pickerContentPort.postMessage({
what: 'togglePreview',
state: false,
});
svgListening(true);
};
/******************************************************************************/
const startPicker = function() {
self.addEventListener('keydown', onKeyPressed, true);
const svg = $stor('svg#sea');
svg.addEventListener('click', onSvgClicked);
svg.addEventListener('touchstart', onSvgTouch);
svg.addEventListener('touchend', onSvgTouch);
unpausePicker();
if ( pickerRoot.classList.contains('zap') ) { return; }
cmEditor.on('changes', onCandidateChanged);
$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);
$id('candidateFilters').addEventListener('click', onCandidateClicked);
$stor('#resultsetDepth input').addEventListener('input', onDepthChanged);
$stor('#resultsetSpecificity input').addEventListener('input', onSpecificityChanged);
staticFilteringParser = new sfp.AstFilterParser({
interactive: true,
nativeCssHas: vAPI.webextFlavor.env.includes('native_css_has'),
});
};
/******************************************************************************/
const quitPicker = function() {
pickerContentPort.postMessage({ what: 'quitPicker' });
pickerContentPort.close();
pickerContentPort = undefined;
};
/******************************************************************************/
const onPickerMessage = function(msg) {
switch ( msg.what ) {
case 'candidatesOptimized':
onCandidatesOptimized(msg);
break;
case 'showDialog':
showDialog(msg);
break;
case 'resultsetDetails': {
resultsetOpt = msg.opt;
$id('resultsetCount').textContent = msg.count;
if ( msg.count !== 0 ) {
$id('create').removeAttribute('disabled');
} else {
$id('create').setAttribute('disabled', '');
}
break;
}
case 'svgPaths': {
let { ocean, islands } = msg;
ocean += islands;
svgOcean.setAttribute('d', ocean);
svgIslands.setAttribute('d', islands || NoPaths);
break;
}
default:
break;
}
};
/******************************************************************************/
// Wait for the content script to establish communication
let pickerContentPort;
globalThis.addEventListener('message', ev => {
const msg = ev.data || {};
if ( msg.what !== 'epickerStart' ) { return; }
if ( Array.isArray(ev.ports) === false ) { return; }
if ( ev.ports.length === 0 ) { return; }
pickerContentPort = ev.ports[0];
pickerContentPort.onmessage = ev => {
const msg = ev.data || {};
onPickerMessage(msg);
};
pickerContentPort.onmessageerror = ( ) => {
quitPicker();
};
startPicker();
pickerContentPort.postMessage({ what: 'start' });
}, { once: true });
/******************************************************************************/
})();

View File

@@ -0,0 +1,132 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2018-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/uMatrix
*/
'use strict';
/******************************************************************************/
export const faIconsInit = (( ) => {
// https://github.com/uBlockOrigin/uBlock-issues/issues/1196
const svgIcons = new Map([
// See /img/fontawesome/fontawesome-defs.svg
[ 'angle-up', { viewBox: '0 0 998 582', path: 'm 998,499 q 0,13 -10,23 l -50,50 q -10,10 -23,10 -13,0 -23,-10 L 499,179 106,572 Q 96,582 83,582 70,582 60,572 L 10,522 Q 0,512 0,499 0,486 10,476 L 476,10 q 10,-10 23,-10 13,0 23,10 l 466,466 q 10,10 10,23 z' } ],
[ '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' } ],
[ 'cloud-upload', { viewBox: '0 0 1920 1408', path: 'm 1280,736 q 0,-14 -9,-23 L 919,361 q -9,-9 -23,-9 -14,0 -23,9 L 522,712 q -10,12 -10,24 0,14 9,23 9,9 23,9 l 224,0 0,352 q 0,13 9.5,22.5 9.5,9.5 22.5,9.5 l 192,0 q 13,0 22.5,-9.5 9.5,-9.5 9.5,-22.5 l 0,-352 224,0 q 13,0 22.5,-9.5 9.5,-9.5 9.5,-22.5 z m 640,288 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' } ],
[ 'check', { viewBox: '0 0 1550 1188', path: 'm 1550,232 q 0,40 -28,68 l -724,724 -136,136 q -28,28 -68,28 -40,0 -68,-28 L 390,1024 28,662 Q 0,634 0,594 0,554 28,526 L 164,390 q 28,-28 68,-28 40,0 68,28 L 594,685 1250,28 q 28,-28 68,-28 40,0 68,28 l 136,136 q 28,28 28,68 z' } ],
[ 'code', { viewBox: '0 0 1830 1373', path: 'm 572,1125.5 -50,50 q -10,10 -23,10 -13,0 -23,-10 l -466,-466 q -10,-10 -10,-23 0,-13 10,-23 l 466,-466 q 10,-10 23,-10 13,0 23,10 l 50,50 q 10,10 10,23 0,13 -10,23 l -393,393 393,393 q 10,10 10,23 0,13 -10,23 z M 1163,58.476203 790,1349.4762 q -4,13 -15.5,19.5 -11.5,6.5 -23.5,2.5 l -62,-17 q -13,-4 -19.5,-15.5 -6.5,-11.5 -2.5,-24.5 L 1040,23.5 q 4,-13 15.5,-19.5 11.5,-6.5 23.5,-2.5 l 62,17 q 13,4 19.5,15.5 6.5,11.5 2.5,24.5 z m 657,651 -466,466 q -10,10 -23,10 -13,0 -23,-10 l -50,-50 q -10,-10 -10,-23 0,-13 10,-23 l 393,-393 -393,-393 q -10,-10 -10,-23 0,-13 10,-23 l 50,-50 q 10,-10 23,-10 13,0 23,10 l 466,466 q 10,10 10,23 0,13 -10,23 z' } ],
[ 'cog', { viewBox: '0 0 1536 1536', path: 'm 1024,768 q 0,-106 -75,-181 -75,-75 -181,-75 -106,0 -181,75 -75,75 -75,181 0,106 75,181 75,75 181,75 106,0 181,-75 75,-75 75,-181 z m 512,-109 0,222 q 0,12 -8,23 -8,11 -20,13 l -185,28 q -19,54 -39,91 35,50 107,138 10,12 10,25 0,13 -9,23 -27,37 -99,108 -72,71 -94,71 -12,0 -26,-9 l -138,-108 q -44,23 -91,38 -16,136 -29,186 -7,28 -36,28 l -222,0 q -14,0 -24.5,-8.5 Q 622,1519 621,1506 l -28,-184 q -49,-16 -90,-37 l -141,107 q -10,9 -25,9 -14,0 -25,-11 -126,-114 -165,-168 -7,-10 -7,-23 0,-12 8,-23 15,-21 51,-66.5 36,-45.5 54,-70.5 -27,-50 -41,-99 L 29,913 Q 16,911 8,900.5 0,890 0,877 L 0,655 q 0,-12 8,-23 8,-11 19,-13 l 186,-28 q 14,-46 39,-92 -40,-57 -107,-138 -10,-12 -10,-24 0,-10 9,-23 26,-36 98.5,-107.5 Q 315,135 337,135 q 13,0 26,10 L 501,252 Q 545,229 592,214 608,78 621,28 628,0 657,0 L 879,0 Q 893,0 903.5,8.5 914,17 915,30 l 28,184 q 49,16 90,37 l 142,-107 q 9,-9 24,-9 13,0 25,10 129,119 165,170 7,8 7,22 0,12 -8,23 -15,21 -51,66.5 -36,45.5 -54,70.5 26,50 41,98 l 183,28 q 13,2 21,12.5 8,10.5 8,23.5 z' } ],
[ 'cogs', { viewBox: '0 0 1920 1761', path: 'm 896,880 q 0,-106 -75,-181 -75,-75 -181,-75 -106,0 -181,75 -75,75 -75,181 0,106 75,181 75,75 181,75 106,0 181,-75 75,-75 75,-181 z m 768,512 q 0,-52 -38,-90 -38,-38 -90,-38 -52,0 -90,38 -38,38 -38,90 0,53 37.5,90.5 37.5,37.5 90.5,37.5 53,0 90.5,-37.5 37.5,-37.5 37.5,-90.5 z m 0,-1024 q 0,-52 -38,-90 -38,-38 -90,-38 -52,0 -90,38 -38,38 -38,90 0,53 37.5,90.5 37.5,37.5 90.5,37.5 53,0 90.5,-37.5 Q 1664,421 1664,368 Z m -384,421 v 185 q 0,10 -7,19.5 -7,9.5 -16,10.5 l -155,24 q -11,35 -32,76 34,48 90,115 7,11 7,20 0,12 -7,19 -23,30 -82.5,89.5 -59.5,59.5 -78.5,59.5 -11,0 -21,-7 l -115,-90 q -37,19 -77,31 -11,108 -23,155 -7,24 -30,24 H 547 q -11,0 -20,-7.5 -9,-7.5 -10,-17.5 l -23,-153 q -34,-10 -75,-31 l -118,89 q -7,7 -20,7 -11,0 -21,-8 -144,-133 -144,-160 0,-9 7,-19 10,-14 41,-53 31,-39 47,-61 -23,-44 -35,-82 L 24,1000 Q 14,999 7,990.5 0,982 0,971 V 786 Q 0,776 7,766.5 14,757 23,756 l 155,-24 q 11,-35 32,-76 -34,-48 -90,-115 -7,-11 -7,-20 0,-12 7,-20 22,-30 82,-89 60,-59 79,-59 11,0 21,7 l 115,90 q 34,-18 77,-32 11,-108 23,-154 7,-24 30,-24 h 186 q 11,0 20,7.5 9,7.5 10,17.5 l 23,153 q 34,10 75,31 l 118,-89 q 8,-7 20,-7 11,0 21,8 144,133 144,160 0,8 -7,19 -12,16 -42,54 -30,38 -45,60 23,48 34,82 l 152,23 q 10,2 17,10.5 7,8.5 7,19.5 z m 640,533 v 140 q 0,16 -149,31 -12,27 -30,52 51,113 51,138 0,4 -4,7 -122,71 -124,71 -8,0 -46,-47 -38,-47 -52,-68 -20,2 -30,2 -10,0 -30,-2 -14,21 -52,68 -38,47 -46,47 -2,0 -124,-71 -4,-3 -4,-7 0,-25 51,-138 -18,-25 -30,-52 -149,-15 -149,-31 v -140 q 0,-16 149,-31 13,-29 30,-52 -51,-113 -51,-138 0,-4 4,-7 4,-2 35,-20 31,-18 59,-34 28,-16 30,-16 8,0 46,46.5 38,46.5 52,67.5 20,-2 30,-2 10,0 30,2 51,-71 92,-112 l 6,-2 q 4,0 124,70 4,3 4,7 0,25 -51,138 17,23 30,52 149,15 149,31 z m 0,-1024 v 140 q 0,16 -149,31 -12,27 -30,52 51,113 51,138 0,4 -4,7 -122,71 -124,71 -8,0 -46,-47 -38,-47 -52,-68 -20,2 -30,2 -10,0 -30,-2 -14,21 -52,68 -38,47 -46,47 -2,0 -124,-71 -4,-3 -4,-7 0,-25 51,-138 -18,-25 -30,-52 -149,-15 -149,-31 V 298 q 0,-16 149,-31 13,-29 30,-52 -51,-113 -51,-138 0,-4 4,-7 4,-2 35,-20 31,-18 59,-34 28,-16 30,-16 8,0 46,46.5 38,46.5 52,67.5 20,-2 30,-2 10,0 30,2 51,-71 92,-112 l 6,-2 q 4,0 124,70 4,3 4,7 0,25 -51,138 17,23 30,52 149,15 149,31 z' } ],
[ 'comment-alt', { viewBox: '0 0 1792 1536', path: 'M 896,128 Q 692,128 514.5,197.5 337,267 232.5,385 128,503 128,640 128,752 199.5,853.5 271,955 401,1029 l 87,50 -27,96 q -24,91 -70,172 152,-63 275,-171 l 43,-38 57,6 q 69,8 130,8 204,0 381.5,-69.5 Q 1455,1013 1559.5,895 1664,777 1664,640 1664,503 1559.5,385 1455,267 1277.5,197.5 1100,128 896,128 Z m 896,512 q 0,174 -120,321.5 -120,147.5 -326,233 -206,85.5 -450,85.5 -70,0 -145,-8 -198,175 -460,242 -49,14 -114,22 h -5 q -15,0 -27,-10.5 -12,-10.5 -16,-27.5 v -1 q -3,-4 -0.5,-12 2.5,-8 2,-10 -0.5,-2 4.5,-9.5 l 6,-9 q 0,0 7,-8.5 7,-8.5 8,-9 7,-8 31,-34.5 24,-26.5 34.5,-38 10.5,-11.5 31,-39.5 20.5,-28 32.5,-51 12,-23 27,-59 15,-36 26,-76 Q 181,1052 90.5,921 0,790 0,640 0,466 120,318.5 240,171 446,85.5 652,0 896,0 q 244,0 450,85.5 206,85.5 326,233 120,147.5 120,321.5 z' } ],
[ 'double-angle-left', { viewBox: '0 0 966 998', path: 'm 582,915 q 0,13 -10,23 l -50,50 q -10,10 -23,10 -13,0 -23,-10 L 10,522 Q 0,512 0,499 0,486 10,476 L 476,10 q 10,-10 23,-10 13,0 23,10 l 50,50 q 10,10 10,23 0,13 -10,23 L 179,499 572,892 q 10,10 10,23 z m 384,0 q 0,13 -10,23 l -50,50 q -10,10 -23,10 -13,0 -23,-10 L 394,522 q -10,-10 -10,-23 0,-13 10,-23 L 860,10 q 10,-10 23,-10 13,0 23,10 l 50,50 q 10,10 10,23 0,13 -10,23 L 563,499 956,892 q 10,10 10,23 z' } ],
[ 'double-angle-up', { viewBox: '0 0 998 966', path: 'm 998,883 q 0,13 -10,23 l -50,50 q -10,10 -23,10 -13,0 -23,-10 L 499,563 106,956 Q 96,966 83,966 70,966 60,956 L 10,906 Q 0,896 0,883 0,870 10,860 L 476,394 q 10,-10 23,-10 13,0 23,10 l 466,466 q 10,10 10,23 z m 0,-384 q 0,13 -10,23 l -50,50 q -10,10 -23,10 -13,0 -23,-10 L 499,179 106,572 Q 96,582 83,582 70,582 60,572 L 10,522 Q 0,512 0,499 0,486 10,476 L 476,10 q 10,-10 23,-10 13,0 23,10 l 466,466 q 10,10 10,23 z' } ],
[ 'download-alt', { viewBox: '0 0 1664 1536', path: 'm 1280,1344 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 465 l 135,136 q 58,56 136,56 78,0 136,-56 l 136,-136 h 464 q 40,0 68,28 28,28 28,68 z M 1339,551 q 17,41 -14,70 l -448,448 q -18,19 -45,19 -27,0 -45,-19 L 339,621 q -31,-29 -14,-70 17,-39 59,-39 H 640 V 64 Q 640,38 659,19 678,0 704,0 h 256 q 26,0 45,19 19,19 19,45 v 448 h 256 q 42,0 59,39 z' } ],
[ 'eraser', { viewBox: '0 0 1920 1280', path: 'M 896,1152 1232,768 l -768,0 -336,384 768,0 z M 1909,75 q 15,34 9.5,71.5 Q 1913,184 1888,212 L 992,1236 q -38,44 -96,44 l -768,0 q -38,0 -69.5,-20.5 -31.5,-20.5 -47.5,-54.5 -15,-34 -9.5,-71.5 5.5,-37.5 30.5,-65.5 L 928,44 Q 966,0 1024,0 l 768,0 q 38,0 69.5,20.5 Q 1893,41 1909,75 Z' } ],
[ 'exclamation-triangle', { viewBox: '0 0 1794 1664', path: 'm 1025.0139,1375 0,-190 q 0,-14 -9.5,-23.5 -9.5,-9.5 -22.5,-9.5 l -192,0 q -13,0 -22.5,9.5 -9.5,9.5 -9.5,23.5 l 0,190 q 0,14 9.5,23.5 9.5,9.5 22.5,9.5 l 192,0 q 13,0 22.5,-9.5 9.5,-9.5 9.5,-23.5 z m -2,-374 18,-459 q 0,-12 -10,-19 -13,-11 -24,-11 l -220,0 q -11,0 -24,11 -10,7 -10,21 l 17,457 q 0,10 10,16.5 10,6.5 24,6.5 l 185,0 q 14,0 23.5,-6.5 9.5,-6.5 10.5,-16.5 z m -14,-934 768,1408 q 35,63 -2,126 -17,29 -46.5,46 -29.5,17 -63.5,17 l -1536,0 q -34,0 -63.5,-17 -29.5,-17 -46.5,-46 -37,-63 -2,-126 L 785.01389,67 q 17,-31 47,-49 30,-18 65,-18 35,0 65,18 30,18 47,49 z' } ],
[ 'external-link', { viewBox: '0 0 1792 1536', path: 'm 1408,928 0,320 q 0,119 -84.5,203.5 Q 1239,1536 1120,1536 l -832,0 Q 169,1536 84.5,1451.5 0,1367 0,1248 L 0,416 Q 0,297 84.5,212.5 169,128 288,128 l 704,0 q 14,0 23,9 9,9 9,23 l 0,64 q 0,14 -9,23 -9,9 -23,9 l -704,0 q -66,0 -113,47 -47,47 -47,113 l 0,832 q 0,66 47,113 47,47 113,47 l 832,0 q 66,0 113,-47 47,-47 47,-113 l 0,-320 q 0,-14 9,-23 9,-9 23,-9 l 64,0 q 14,0 23,9 9,9 9,23 z m 384,-864 0,512 q 0,26 -19,45 -19,19 -45,19 -26,0 -45,-19 L 1507,445 855,1097 q -10,10 -23,10 -13,0 -23,-10 L 695,983 q -10,-10 -10,-23 0,-13 10,-23 L 1347,285 1171,109 q -19,-19 -19,-45 0,-26 19,-45 19,-19 45,-19 l 512,0 q 26,0 45,19 19,19 19,45 z' } ],
[ 'eye-dropper', { viewBox: '0 0 1792 1792', path: 'm 1698,94 q 94,94 94,226.5 0,132.5 -94,225.5 l -225,223 104,104 q 10,10 10,23 0,13 -10,23 l -210,210 q -10,10 -23,10 -13,0 -23,-10 l -105,-105 -603,603 q -37,37 -90,37 l -203,0 -256,128 -64,-64 128,-256 0,-203 q 0,-53 37,-90 L 768,576 663,471 q -10,-10 -10,-23 0,-13 10,-23 L 873,215 q 10,-10 23,-10 13,0 23,10 L 1023,319 1246,94 Q 1339,0 1471.5,0 1604,0 1698,94 Z M 512,1472 1088,896 896,704 l -576,576 0,192 192,0 z' } ],
[ 'eye-open', { viewBox: '0 0 1792 1152', path: 'm 1664,576 q -152,-236 -381,-353 61,104 61,225 0,185 -131.5,316.5 Q 1081,896 896,896 711,896 579.5,764.5 448,633 448,448 448,327 509,223 280,340 128,576 261,781 461.5,902.5 662,1024 896,1024 1130,1024 1330.5,902.5 1531,781 1664,576 Z M 944,192 q 0,-20 -14,-34 -14,-14 -34,-14 -125,0 -214.5,89.5 Q 592,323 592,448 q 0,20 14,34 14,14 34,14 20,0 34,-14 14,-14 14,-34 0,-86 61,-147 61,-61 147,-61 20,0 34,-14 14,-14 14,-34 z m 848,384 q 0,34 -20,69 -140,230 -376.5,368.5 Q 1159,1152 896,1152 633,1152 396.5,1013 160,874 20,645 0,610 0,576 0,542 20,507 160,278 396.5,139 633,0 896,0 q 263,0 499.5,139 236.5,139 376.5,368 20,35 20,69 z' } ],
[ 'eye-slash', { viewBox: '0 0 1792 1344', path: 'M 555,1047 633,906 Q 546,843 497,747 448,651 448,544 448,423 509,319 280,436 128,672 295,930 555,1047 Z M 944,288 q 0,-20 -14,-34 -14,-14 -34,-14 -125,0 -214.5,89.5 Q 592,419 592,544 q 0,20 14,34 14,14 34,14 20,0 34,-14 14,-14 14,-34 0,-86 61,-147 61,-61 147,-61 20,0 34,-14 14,-14 14,-34 z M 1307,97 q 0,7 -1,9 -106,189 -316,567 -210,378 -315,566 l -49,89 q -10,16 -28,16 -12,0 -134,-70 -16,-10 -16,-28 0,-12 44,-87 Q 349,1094 228.5,986 108,878 20,741 0,710 0,672 0,634 20,603 173,368 400,232 627,96 896,96 q 89,0 180,17 l 54,-97 q 10,-16 28,-16 5,0 18,6 13,6 31,15.5 18,9.5 33,18.5 15,9 31.5,18.5 16.5,9.5 19.5,11.5 16,10 16,27 z m 37,447 q 0,139 -79,253.5 Q 1186,912 1056,962 l 280,-502 q 8,45 8,84 z m 448,128 q 0,35 -20,69 -39,64 -109,145 -150,172 -347.5,267 -197.5,95 -419.5,95 l 74,-132 Q 1182,1098 1362.5,979 1543,860 1664,672 1549,493 1382,378 l 63,-112 q 95,64 182.5,153 87.5,89 144.5,184 20,34 20,69 z' } ],
[ 'files-o', { viewBox: '0 0 1792 1792', path: 'm 1696,384 q 40,0 68,28 28,28 28,68 l 0,1216 q 0,40 -28,68 -28,28 -68,28 l -960,0 q -40,0 -68,-28 -28,-28 -28,-68 l 0,-288 -544,0 Q 56,1408 28,1380 0,1352 0,1312 L 0,640 Q 0,600 20,552 40,504 68,476 L 476,68 Q 504,40 552,20 600,0 640,0 l 416,0 q 40,0 68,28 28,28 28,68 l 0,328 q 68,-40 128,-40 l 416,0 z m -544,213 -299,299 299,0 0,-299 z M 512,213 213,512 l 299,0 0,-299 z m 196,647 316,-316 0,-416 -384,0 0,416 q 0,40 -28,68 -28,28 -68,28 l -416,0 0,640 512,0 0,-256 q 0,-40 20,-88 20,-48 48,-76 z m 956,804 0,-1152 -384,0 0,416 q 0,40 -28,68 -28,28 -68,28 l -416,0 0,640 896,0 z' } ],
[ 'film', { viewBox: '0 0 1920 1664', path: 'm 384,1472 0,-128 q 0,-26 -19,-45 -19,-19 -45,-19 l -128,0 q -26,0 -45,19 -19,19 -19,45 l 0,128 q 0,26 19,45 19,19 45,19 l 128,0 q 26,0 45,-19 19,-19 19,-45 z m 0,-384 0,-128 q 0,-26 -19,-45 -19,-19 -45,-19 l -128,0 q -26,0 -45,19 -19,19 -19,45 l 0,128 q 0,26 19,45 19,19 45,19 l 128,0 q 26,0 45,-19 19,-19 19,-45 z m 0,-384 0,-128 q 0,-26 -19,-45 -19,-19 -45,-19 l -128,0 q -26,0 -45,19 -19,19 -19,45 l 0,128 q 0,26 19,45 19,19 45,19 l 128,0 q 26,0 45,-19 19,-19 19,-45 z m 1024,768 0,-512 q 0,-26 -19,-45 -19,-19 -45,-19 l -768,0 q -26,0 -45,19 -19,19 -19,45 l 0,512 q 0,26 19,45 19,19 45,19 l 768,0 q 26,0 45,-19 19,-19 19,-45 z M 384,320 384,192 q 0,-26 -19,-45 -19,-19 -45,-19 l -128,0 q -26,0 -45,19 -19,19 -19,45 l 0,128 q 0,26 19,45 19,19 45,19 l 128,0 q 26,0 45,-19 19,-19 19,-45 z m 1408,1152 0,-128 q 0,-26 -19,-45 -19,-19 -45,-19 l -128,0 q -26,0 -45,19 -19,19 -19,45 l 0,128 q 0,26 19,45 19,19 45,19 l 128,0 q 26,0 45,-19 19,-19 19,-45 z m -384,-768 0,-512 q 0,-26 -19,-45 -19,-19 -45,-19 l -768,0 q -26,0 -45,19 -19,19 -19,45 l 0,512 q 0,26 19,45 19,19 45,19 l 768,0 q 26,0 45,-19 19,-19 19,-45 z m 384,384 0,-128 q 0,-26 -19,-45 -19,-19 -45,-19 l -128,0 q -26,0 -45,19 -19,19 -19,45 l 0,128 q 0,26 19,45 19,19 45,19 l 128,0 q 26,0 45,-19 19,-19 19,-45 z m 0,-384 0,-128 q 0,-26 -19,-45 -19,-19 -45,-19 l -128,0 q -26,0 -45,19 -19,19 -19,45 l 0,128 q 0,26 19,45 19,19 45,19 l 128,0 q 26,0 45,-19 19,-19 19,-45 z m 0,-384 0,-128 q 0,-26 -19,-45 -19,-19 -45,-19 l -128,0 q -26,0 -45,19 -19,19 -19,45 l 0,128 q 0,26 19,45 19,19 45,19 l 128,0 q 26,0 45,-19 19,-19 19,-45 z m 128,-160 0,1344 q 0,66 -47,113 -47,47 -113,47 l -1600,0 Q 94,1664 47,1617 0,1570 0,1504 L 0,160 Q 0,94 47,47 94,0 160,0 l 1600,0 q 66,0 113,47 47,47 47,113 z' } ],
[ 'filter', { viewBox: '0 0 1410 1408', path: 'm 1404.0208,39 q 17,41 -14,70 l -493,493 0,742 q 0,42 -39,59 -13,5 -25,5 -27,0 -45,-19 l -256,-256 q -19,-19 -19,-45 l 0,-486 L 20.020833,109 q -31,-29 -14,-70 Q 23.020833,0 65.020833,0 L 1345.0208,0 q 42,0 59,39 z' } ],
[ 'floppy-o', { viewBox: '0 0 1536 1536', path: 'm 384,1408 768,0 0,-384 -768,0 0,384 z m 896,0 128,0 0,-896 q 0,-14 -10,-38.5 Q 1388,449 1378,439 L 1097,158 q -10,-10 -34,-20 -24,-10 -39,-10 l 0,416 q 0,40 -28,68 -28,28 -68,28 l -576,0 q -40,0 -68,-28 -28,-28 -28,-68 l 0,-416 -128,0 0,1280 128,0 0,-416 q 0,-40 28,-68 28,-28 68,-28 l 832,0 q 40,0 68,28 28,28 28,68 l 0,416 z M 896,480 896,160 q 0,-13 -9.5,-22.5 Q 877,128 864,128 l -192,0 q -13,0 -22.5,9.5 Q 640,147 640,160 l 0,320 q 0,13 9.5,22.5 9.5,9.5 22.5,9.5 l 192,0 q 13,0 22.5,-9.5 Q 896,493 896,480 Z m 640,32 0,928 q 0,40 -28,68 -28,28 -68,28 L 96,1536 Q 56,1536 28,1508 0,1480 0,1440 L 0,96 Q 0,56 28,28 56,0 96,0 l 928,0 q 40,0 88,20 48,20 76,48 l 280,280 q 28,28 48,76 20,48 20,88 z' } ],
[ 'font', { viewBox: '0 0 1664 1536', path: 'M 725,431 555,881 q 33,0 136.5,2 103.5,2 160.5,2 19,0 57,-2 Q 822,630 725,431 Z M 0,1536 2,1457 q 23,-7 56,-12.5 33,-5.5 57,-10.5 24,-5 49.5,-14.5 25.5,-9.5 44.5,-29 19,-19.5 31,-50.5 L 477,724 757,0 l 75,0 53,0 q 8,14 11,21 l 205,480 q 33,78 106,257.5 73,179.5 114,274.5 15,34 58,144.5 43,110.5 72,168.5 20,45 35,57 19,15 88,29.5 69,14.5 84,20.5 6,38 6,57 0,5 -0.5,13.5 -0.5,8.5 -0.5,12.5 -63,0 -190,-8 -127,-8 -191,-8 -76,0 -215,7 -139,7 -178,8 0,-43 4,-78 l 131,-28 q 1,0 12.5,-2.5 11.5,-2.5 15.5,-3.5 4,-1 14.5,-4.5 10.5,-3.5 15,-6.5 4.5,-3 11,-8 6.5,-5 9,-11 2.5,-6 2.5,-14 0,-16 -31,-96.5 -31,-80.5 -72,-177.5 -41,-97 -42,-100 l -450,-2 q -26,58 -76.5,195.5 Q 382,1336 382,1361 q 0,22 14,37.5 14,15.5 43.5,24.5 29.5,9 48.5,13.5 19,4.5 57,8.5 38,4 41,4 1,19 1,58 0,9 -2,27 -58,0 -174.5,-10 -116.5,-10 -174.5,-10 -8,0 -26.5,4 -18.5,4 -21.5,4 -80,14 -188,14 z' } ],
[ 'home', { viewBox: '0 0 1612 1283', path: 'm 1382.1111,739 v 480 q 0,26 -19,45 -19,19 -45,19 H 934.11111 V 899 h -256 v 384 h -384 q -26,0 -45,-19 -19,-19 -19,-45 V 739 q 0,-1 0.5,-3 0.5,-2 0.5,-3 l 575,-474 574.99999,474 q 1,2 1,6 z m 223,-69 -62,74 q -8,9 -21,11 h -3 q -13,0 -21,-7 l -691.99999,-577 -692,577 q -12,8 -23.999999,7 -13,-2 -21,-11 L 7.1111111,670 Q -0.88888889,660 0.11111111,646.5 1.1111111,633 11.111111,625 L 730.11111,26 q 32,-26 76,-26 44,0 76,26 L 1126.1111,230 V 35 q 0,-14 9,-23 9,-9 23,-9 h 192 q 14,0 23,9 9,9 9,23 v 408 l 219,182 q 10,8 11,21.5 1,13.5 -7,23.5 z' } ],
[ 'info-circle', { viewBox: '0 0 1536 1536', path: 'm 1024,1248 0,-160 q 0,-14 -9,-23 -9,-9 -23,-9 l -96,0 0,-512 q 0,-14 -9,-23 -9,-9 -23,-9 l -320,0 q -14,0 -23,9 -9,9 -9,23 l 0,160 q 0,14 9,23 9,9 23,9 l 96,0 0,320 -96,0 q -14,0 -23,9 -9,9 -9,23 l 0,160 q 0,14 9,23 9,9 23,9 l 448,0 q 14,0 23,-9 9,-9 9,-23 z M 896,352 896,192 q 0,-14 -9,-23 -9,-9 -23,-9 l -192,0 q -14,0 -23,9 -9,9 -9,23 l 0,160 q 0,14 9,23 9,9 23,9 l 192,0 q 14,0 23,-9 9,-9 9,-23 z m 640,416 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' } ],
[ 'list-alt', { viewBox: '0 0 1792 1408', path: 'm 384,1056 0,64 q 0,13 -9.5,22.5 -9.5,9.5 -22.5,9.5 l -64,0 q -13,0 -22.5,-9.5 Q 256,1133 256,1120 l 0,-64 q 0,-13 9.5,-22.5 9.5,-9.5 22.5,-9.5 l 64,0 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 0,-256 0,64 q 0,13 -9.5,22.5 Q 365,896 352,896 l -64,0 q -13,0 -22.5,-9.5 Q 256,877 256,864 l 0,-64 q 0,-13 9.5,-22.5 Q 275,768 288,768 l 64,0 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 0,-256 0,64 q 0,13 -9.5,22.5 Q 365,640 352,640 l -64,0 q -13,0 -22.5,-9.5 Q 256,621 256,608 l 0,-64 q 0,-13 9.5,-22.5 Q 275,512 288,512 l 64,0 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 1152,512 0,64 q 0,13 -9.5,22.5 -9.5,9.5 -22.5,9.5 l -960,0 q -13,0 -22.5,-9.5 Q 512,1133 512,1120 l 0,-64 q 0,-13 9.5,-22.5 9.5,-9.5 22.5,-9.5 l 960,0 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 0,-256 0,64 q 0,13 -9.5,22.5 -9.5,9.5 -22.5,9.5 l -960,0 q -13,0 -22.5,-9.5 Q 512,877 512,864 l 0,-64 q 0,-13 9.5,-22.5 Q 531,768 544,768 l 960,0 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 0,-256 0,64 q 0,13 -9.5,22.5 -9.5,9.5 -22.5,9.5 l -960,0 q -13,0 -22.5,-9.5 Q 512,621 512,608 l 0,-64 q 0,-13 9.5,-22.5 Q 531,512 544,512 l 960,0 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 128,704 0,-832 q 0,-13 -9.5,-22.5 Q 1645,384 1632,384 l -1472,0 q -13,0 -22.5,9.5 Q 128,403 128,416 l 0,832 q 0,13 9.5,22.5 9.5,9.5 22.5,9.5 l 1472,0 q 13,0 22.5,-9.5 9.5,-9.5 9.5,-22.5 z m 128,-1088 0,1088 q 0,66 -47,113 -47,47 -113,47 l -1472,0 Q 94,1408 47,1361 0,1314 0,1248 L 0,160 Q 0,94 47,47 94,0 160,0 l 1472,0 q 66,0 113,47 47,47 47,113 z' } ],
[ 'lock', { viewBox: '0 0 1152 1408', path: 'm 320,640 512,0 0,-192 q 0,-106 -75,-181 -75,-75 -181,-75 -106,0 -181,75 -75,75 -75,181 l 0,192 z m 832,96 0,576 q 0,40 -28,68 -28,28 -68,28 l -960,0 Q 56,1408 28,1380 0,1352 0,1312 L 0,736 q 0,-40 28,-68 28,-28 68,-28 l 32,0 0,-192 Q 128,264 260,132 392,0 576,0 q 184,0 316,132 132,132 132,316 l 0,192 32,0 q 40,0 68,28 28,28 28,68 z' } ],
[ 'magic', { viewBox: '0 0 1637 1637', path: 'M 1163,581 1456,288 1349,181 1056,474 Z m 447,-293 q 0,27 -18,45 L 306,1619 q -18,18 -45,18 -27,0 -45,-18 L 18,1421 Q 0,1403 0,1376 0,1349 18,1331 L 1304,45 q 18,-18 45,-18 27,0 45,18 l 198,198 q 18,18 18,45 z M 259,98 l 98,30 -98,30 -30,98 -30,-98 -98,-30 98,-30 30,-98 z M 609,260 805,320 609,380 549,576 489,380 293,320 489,260 549,64 Z m 930,478 98,30 -98,30 -30,98 -30,-98 -98,-30 98,-30 30,-98 z M 899,98 l 98,30 -98,30 -30,98 -30,-98 -98,-30 98,-30 30,-98 z' } ],
[ 'pause-circle-o', { viewBox: '0 0 1536 1536', path: 'M 768,0 Q 977,0 1153.5,103 1330,206 1433,382.5 1536,559 1536,768 1536,977 1433,1153.5 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 Z m 0,1312 q 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 0,148 73,273 73,125 198,198 125,73 273,73 z m 96,-224 q -14,0 -23,-9 -9,-9 -9,-23 l 0,-576 q 0,-14 9,-23 9,-9 23,-9 l 192,0 q 14,0 23,9 9,9 9,23 l 0,576 q 0,14 -9,23 -9,9 -23,9 l -192,0 z m -384,0 q -14,0 -23,-9 -9,-9 -9,-23 l 0,-576 q 0,-14 9,-23 9,-9 23,-9 l 192,0 q 14,0 23,9 9,9 9,23 l 0,576 q 0,14 -9,23 -9,9 -23,9 l -192,0 z' } ],
[ 'play-circle-o', { viewBox: '0 0 1536 1536', path: 'm 1184,768 q 0,37 -32,55 l -544,320 q -15,9 -32,9 -16,0 -32,-8 -32,-19 -32,-56 l 0,-640 q 0,-37 32,-56 33,-18 64,1 l 544,320 q 32,18 32,55 z m 128,0 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' } ],
[ 'plus', { viewBox: '0 0 1408 1408', path: 'm 1408,608 0,192 q 0,40 -28,68 -28,28 -68,28 l -416,0 0,416 q 0,40 -28,68 -28,28 -68,28 l -192,0 q -40,0 -68,-28 -28,-28 -28,-68 l 0,-416 -416,0 Q 56,896 28,868 0,840 0,800 L 0,608 q 0,-40 28,-68 28,-28 68,-28 l 416,0 0,-416 Q 512,56 540,28 568,0 608,0 l 192,0 q 40,0 68,28 28,28 28,68 l 0,416 416,0 q 40,0 68,28 28,28 28,68 z' } ],
[ 'power-off', { viewBox: '0 0 1536 1664', path: 'm 1536,896 q 0,156 -61,298 -61,142 -164,245 -103,103 -245,164 -142,61 -298,61 -156,0 -298,-61 Q 328,1542 225,1439 122,1336 61,1194 0,1052 0,896 0,714 80.5,553 161,392 307,283 q 43,-32 95.5,-25 52.5,7 83.5,50 32,42 24.5,94.5 Q 503,455 461,487 363,561 309.5,668 256,775 256,896 q 0,104 40.5,198.5 40.5,94.5 109.5,163.5 69,69 163.5,109.5 94.5,40.5 198.5,40.5 104,0 198.5,-40.5 Q 1061,1327 1130,1258 1199,1189 1239.5,1094.5 1280,1000 1280,896 1280,775 1226.5,668 1173,561 1075,487 1033,455 1025.5,402.5 1018,350 1050,308 q 31,-43 84,-50 53,-7 95,25 146,109 226.5,270 80.5,161 80.5,343 z m -640,-768 0,640 q 0,52 -38,90 -38,38 -90,38 -52,0 -90,-38 -38,-38 -38,-90 l 0,-640 q 0,-52 38,-90 38,-38 90,-38 52,0 90,38 38,38 38,90 z' } ],
[ 'question-circle', { viewBox: '0 0 1536 1536', path: 'm 896,1248 v -192 q 0,-14 -9,-23 -9,-9 -23,-9 H 672 q -14,0 -23,9 -9,9 -9,23 v 192 q 0,14 9,23 9,9 23,9 h 192 q 14,0 23,-9 9,-9 9,-23 z m 256,-672 q 0,-88 -55.5,-163 Q 1041,338 958,297 875,256 788,256 q -243,0 -371,213 -15,24 8,42 l 132,100 q 7,6 19,6 16,0 25,-12 53,-68 86,-92 34,-24 86,-24 48,0 85.5,26 37.5,26 37.5,59 0,38 -20,61 -20,23 -68,45 -63,28 -115.5,86.5 Q 640,825 640,892 v 36 q 0,14 9,23 9,9 23,9 h 192 q 14,0 23,-9 9,-9 9,-23 0,-19 21.5,-49.5 Q 939,848 972,829 q 32,-18 49,-28.5 17,-10.5 46,-35 29,-24.5 44.5,-48 15.5,-23.5 28,-60.5 12.5,-37 12.5,-81 z m 384,192 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' } ],
[ 'refresh', { viewBox: '0 0 1536 1536', path: 'm 1511,928 q 0,5 -1,7 -64,268 -268,434.5 Q 1038,1536 764,1536 618,1536 481.5,1481 345,1426 238,1324 l -129,129 q -19,19 -45,19 -26,0 -45,-19 Q 0,1434 0,1408 L 0,960 q 0,-26 19,-45 19,-19 45,-19 l 448,0 q 26,0 45,19 19,19 19,45 0,26 -19,45 l -137,137 q 71,66 161,102 90,36 187,36 134,0 250,-65 116,-65 186,-179 11,-17 53,-117 8,-23 30,-23 l 192,0 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 25,-800 0,448 q 0,26 -19,45 -19,19 -45,19 l -448,0 q -26,0 -45,-19 -19,-19 -19,-45 0,-26 19,-45 L 1117,393 Q 969,256 768,256 q -134,0 -250,65 -116,65 -186,179 -11,17 -53,117 -8,23 -30,23 L 50,640 Q 37,640 27.5,630.5 18,621 18,608 l 0,-7 Q 83,333 288,166.5 493,0 768,0 914,0 1052,55.5 1190,111 1297,212 L 1427,83 q 19,-19 45,-19 26,0 45,19 19,19 19,45 z' } ],
[ 'save', { viewBox: '0 0 1536 1536', path: 'm 384,1408 h 768 V 1024 H 384 Z m 896,0 h 128 V 512 q 0,-14 -10,-38.5 Q 1388,449 1378,439 L 1097,158 q -10,-10 -34,-20 -24,-10 -39,-10 v 416 q 0,40 -28,68 -28,28 -68,28 H 352 q -40,0 -68,-28 -28,-28 -28,-68 V 128 H 128 V 1408 H 256 V 992 q 0,-40 28,-68 28,-28 68,-28 h 832 q 40,0 68,28 28,28 28,68 z M 896,480 V 160 q 0,-13 -9.5,-22.5 Q 877,128 864,128 H 672 q -13,0 -22.5,9.5 Q 640,147 640,160 v 320 q 0,13 9.5,22.5 9.5,9.5 22.5,9.5 h 192 q 13,0 22.5,-9.5 Q 896,493 896,480 Z m 640,32 v 928 q 0,40 -28,68 -28,28 -68,28 H 96 Q 56,1536 28,1508 0,1480 0,1440 V 96 Q 0,56 28,28 56,0 96,0 h 928 q 40,0 88,20 48,20 76,48 l 280,280 q 28,28 48,76 20,48 20,88 z' } ],
[ 'search', { viewBox: '0 0 1664 1664', path: 'M 1152,704 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,52 -38,90 -38,38 -90,38 -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' } ],
[ 'sliders', { viewBox: '0 0 1536 1408', path: 'm 352,1152 0,128 -352,0 0,-128 352,0 z m 352,-128 q 26,0 45,19 19,19 19,45 l 0,256 q 0,26 -19,45 -19,19 -45,19 l -256,0 q -26,0 -45,-19 -19,-19 -19,-45 l 0,-256 q 0,-26 19,-45 19,-19 45,-19 l 256,0 z m 160,-384 0,128 -864,0 0,-128 864,0 z m -640,-512 0,128 -224,0 0,-128 224,0 z m 1312,1024 0,128 -736,0 0,-128 736,0 z M 576,0 q 26,0 45,19 19,19 19,45 l 0,256 q 0,26 -19,45 -19,19 -45,19 l -256,0 q -26,0 -45,-19 -19,-19 -19,-45 L 256,64 Q 256,38 275,19 294,0 320,0 l 256,0 z m 640,512 q 26,0 45,19 19,19 19,45 l 0,256 q 0,26 -19,45 -19,19 -45,19 l -256,0 q -26,0 -45,-19 -19,-19 -19,-45 l 0,-256 q 0,-26 19,-45 19,-19 45,-19 l 256,0 z m 320,128 0,128 -224,0 0,-128 224,0 z m 0,-512 0,128 -864,0 0,-128 864,0 z' } ],
[ 'spinner', { viewBox: '0 0 1664 1728', path: 'm 462,1394 q 0,53 -37.5,90.5 -37.5,37.5 -90.5,37.5 -52,0 -90,-38 -38,-38 -38,-90 0,-53 37.5,-90.5 37.5,-37.5 90.5,-37.5 53,0 90.5,37.5 37.5,37.5 37.5,90.5 z m 498,206 q 0,53 -37.5,90.5 Q 885,1728 832,1728 779,1728 741.5,1690.5 704,1653 704,1600 q 0,-53 37.5,-90.5 37.5,-37.5 90.5,-37.5 53,0 90.5,37.5 Q 960,1547 960,1600 Z M 256,896 q 0,53 -37.5,90.5 Q 181,1024 128,1024 75,1024 37.5,986.5 0,949 0,896 0,843 37.5,805.5 75,768 128,768 q 53,0 90.5,37.5 Q 256,843 256,896 Z m 1202,498 q 0,52 -38,90 -38,38 -90,38 -53,0 -90.5,-37.5 -37.5,-37.5 -37.5,-90.5 0,-53 37.5,-90.5 37.5,-37.5 90.5,-37.5 53,0 90.5,37.5 37.5,37.5 37.5,90.5 z M 494,398 q 0,66 -47,113 -47,47 -113,47 -66,0 -113,-47 -47,-47 -47,-113 0,-66 47,-113 47,-47 113,-47 66,0 113,47 47,47 47,113 z m 1170,498 q 0,53 -37.5,90.5 -37.5,37.5 -90.5,37.5 -53,0 -90.5,-37.5 Q 1408,949 1408,896 q 0,-53 37.5,-90.5 37.5,-37.5 90.5,-37.5 53,0 90.5,37.5 Q 1664,843 1664,896 Z M 1024,192 q 0,80 -56,136 -56,56 -136,56 -80,0 -136,-56 -56,-56 -56,-136 0,-80 56,-136 56,-56 136,-56 80,0 136,56 56,56 56,136 z m 530,206 q 0,93 -66,158.5 -66,65.5 -158,65.5 -93,0 -158.5,-65.5 Q 1106,491 1106,398 q 0,-92 65.5,-158 65.5,-66 158.5,-66 92,0 158,66 66,66 66,158 z' } ],
[ 'sun', { viewBox: '0 0 1708 1792', path: 'm 1706,1172.5 c -3,10 -11,17 -20,20 l -292,96 v 306 c 0,10 -5,20 -13,26 -9,6 -19,8 -29,4 l -292,-94 -180,248 c -6,8 -16,13 -26,13 -10,0 -20,-5 -26,-13 l -180,-248 -292,94 c -10,4 -20,2 -29,-4 -8,-6 -13,-16 -13,-26 v -306 l -292,-96 c -9,-3 -17,-10 -20,-20 -3,-10 -2,-21 4,-29 l 180,-248 -180,-248 c -6,-9 -7,-19 -4,-29 3,-10 11,-17 20,-20 l 292,-96 v -306 c 0,-10 5,-20 13,-26 9,-6 19,-8 29,-4 l 292,94 180,-248 c 12,-16 40,-16 52,0 L 1060,260.5 l 292,-94 c 10,-4 20,-2 29,4 8,6 13,16 13,26 v 306 l 292,96 c 9,3 17,10 20,20 3,10 2,20 -4,29 l -180,248 180,248 c 6,8 7,19 4,29 z' } ],
[ 'sun-o', { viewBox: '0 0 1708 1792', path: 'm 1430,895.5 c 0,-318 -258,-576 -576,-576 -318,0 -576,258 -576,576 0,318 258,576 576,576 C 1172,1471.5 1430,1213.5 1430,895.5 Z m 276,277 c -3,10 -11,17 -20,20 l -292,96 v 306 c 0,10 -5,20 -13,26 -9,6 -19,8 -29,4 l -292,-94 -180,248 c -6,8 -16,13 -26,13 -10,0 -20,-5 -26,-13 l -180,-248 -292,94 c -10,4 -20,2 -29,-4 -8,-6 -13,-16 -13,-26 v -306 l -292,-96 c -9,-3 -17,-10 -20,-20 -3,-10 -2,-21 4,-29 l 180,-248 -180,-248 c -6,-9 -7,-19 -4,-29 3,-10 11,-17 20,-20 l 292,-96 v -306 c 0,-10 5,-20 13,-26 9,-6 19,-8 29,-4 l 292,94 180,-248 c 12,-16 40,-16 52,0 L 1060,260.5 l 292,-94 c 10,-4 20,-2 29,4 8,6 13,16 13,26 v 306 l 292,96 c 9,3 17,10 20,20 3,10 2,20 -4,29 l -180,248 180,248 c 6,8 7,19 4,29 z' } ],
[ 'terminal', { viewBox: '0 0 1651 1075', path: 'm572 522-466 466q-10 10-23 10t-23-10l-50-50q-10-10-10-23t10-23l393-393-393-393q-10-10-10-23t10-23l50-50q10-10 23-10t23 10l466 466q10 10 10 23t-10 23zm1079 457v64q0 14-9 23t-23 9h-960q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h960q14 0 23 9t9 23z' } ],
[ 'times', { viewBox: '0 0 1188 1188', path: 'm 1188,956 q 0,40 -28,68 l -136,136 q -28,28 -68,28 -40,0 -68,-28 L 594,866 300,1160 q -28,28 -68,28 -40,0 -68,-28 L 28,1024 Q 0,996 0,956 0,916 28,888 L 322,594 28,300 Q 0,272 0,232 0,192 28,164 L 164,28 Q 192,0 232,0 272,0 300,28 L 594,322 888,28 q 28,-28 68,-28 40,0 68,28 l 136,136 q 28,28 28,68 0,40 -28,68 l -294,294 294,294 q 28,28 28,68 z' } ],
[ 'trash-o', { viewBox: '0 0 1408 1536', path: 'm 512,608 v 576 q 0,14 -9,23 -9,9 -23,9 h -64 q -14,0 -23,-9 -9,-9 -9,-23 V 608 q 0,-14 9,-23 9,-9 23,-9 h 64 q 14,0 23,9 9,9 9,23 z m 256,0 v 576 q 0,14 -9,23 -9,9 -23,9 h -64 q -14,0 -23,-9 -9,-9 -9,-23 V 608 q 0,-14 9,-23 9,-9 23,-9 h 64 q 14,0 23,9 9,9 9,23 z m 256,0 v 576 q 0,14 -9,23 -9,9 -23,9 h -64 q -14,0 -23,-9 -9,-9 -9,-23 V 608 q 0,-14 9,-23 9,-9 23,-9 h 64 q 14,0 23,9 9,9 9,23 z m 128,724 V 384 H 256 v 948 q 0,22 7,40.5 7,18.5 14.5,27 7.5,8.5 10.5,8.5 h 832 q 3,0 10.5,-8.5 7.5,-8.5 14.5,-27 7,-18.5 7,-40.5 z M 480,256 H 928 L 880,139 q -7,-9 -17,-11 H 546 q -10,2 -17,11 z m 928,32 v 64 q 0,14 -9,23 -9,9 -23,9 h -96 v 948 q 0,83 -47,143.5 -47,60.5 -113,60.5 H 288 q -66,0 -113,-58.5 Q 128,1419 128,1336 V 384 H 32 Q 18,384 9,375 0,366 0,352 v -64 q 0,-14 9,-23 9,-9 23,-9 H 341 L 411,89 Q 426,52 465,26 504,0 544,0 h 320 q 40,0 79,26 39,26 54,63 l 70,167 h 309 q 14,0 23,9 9,9 9,23 z' } ],
[ 'undo', { viewBox: '0 0 1536 1536', path: 'm 1536,768 q 0,156 -61,298 -61,142 -164,245 -103,103 -245,164 -142,61 -298,61 -172,0 -327,-72.5 Q 286,1391 177,1259 q -7,-10 -6.5,-22.5 0.5,-12.5 8.5,-20.5 l 137,-138 q 10,-9 25,-9 16,2 23,12 73,95 179,147 106,52 225,52 104,0 198.5,-40.5 Q 1061,1199 1130,1130 1199,1061 1239.5,966.5 1280,872 1280,768 1280,664 1239.5,569.5 1199,475 1130,406 1061,337 966.5,296.5 872,256 768,256 670,256 580,291.5 490,327 420,393 l 137,138 q 31,30 14,69 -17,40 -59,40 H 64 Q 38,640 19,621 0,602 0,576 V 128 Q 0,86 40,69 79,52 109,83 L 239,212 Q 346,111 483.5,55.5 621,0 768,0 q 156,0 298,61 142,61 245,164 103,103 164,245 61,142 61,298 z' } ],
[ '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
[ 'ph-popups', { viewBox: '0 0 20 20', path: 'm 3.146,1.8546316 a 0.5006316,0.5006316 0 0 0 0.708,-0.708 l -1,-1 a 0.5006316,0.5006316 0 0 0 -0.708,0.708 z m -0.836,2.106 a 0.406,0.406 0 0 0 0.19,0.04 0.5,0.5 0 0 0 0.35,-0.851 0.493,0.493 0 0 0 -0.54,-0.109 0.361,0.361 0 0 0 -0.16,0.109 0.485,0.485 0 0 0 0,0.7 0.372,0.372 0 0 0 0.16,0.111 z m 3,-3 a 0.406,0.406 0 0 0 0.19,0.04 0.513,0.513 0 0 0 0.5,-0.5 0.473,0.473 0 0 0 -0.15,-0.351 0.5,0.5 0 0 0 -0.7,0 0.485,0.485 0 0 0 0,0.7 0.372,0.372 0 0 0 0.16,0.111 z m 13.19,1.04 a 0.5,0.5 0 0 0 0.354,-0.146 l 1,-1 a 0.5006316,0.5006316 0 0 0 -0.708,-0.708 l -1,1 a 0.5,0.5 0 0 0 0.354,0.854 z m 1.35,1.149 a 0.361,0.361 0 0 0 -0.16,-0.109 0.5,0.5 0 0 0 -0.38,0 0.361,0.361 0 0 0 -0.16,0.109 0.485,0.485 0 0 0 0,0.7 0.372,0.372 0 0 0 0.16,0.11 0.471,0.471 0 0 0 0.38,0 0.372,0.372 0 0 0 0.16,-0.11 0.469,0.469 0 0 0 0.15,-0.349 0.43,0.43 0 0 0 -0.04,-0.19 0.358,0.358 0 0 0 -0.11,-0.161 z m -3.54,-2.189 a 0.406,0.406 0 0 0 0.19,0.04 0.469,0.469 0 0 0 0.35,-0.15 0.353,0.353 0 0 0 0.11,-0.161 0.469,0.469 0 0 0 0,-0.379 0.358,0.358 0 0 0 -0.11,-0.161 0.361,0.361 0 0 0 -0.16,-0.109 0.493,0.493 0 0 0 -0.54,0.109 0.358,0.358 0 0 0 -0.11,0.161 0.43,0.43 0 0 0 -0.04,0.19 0.469,0.469 0 0 0 0.15,0.35 0.372,0.372 0 0 0 0.16,0.11 z m 2.544,15.1860004 a 0.5006316,0.5006316 0 0 0 -0.708,0.708 l 1,1 a 0.5006316,0.5006316 0 0 0 0.708,-0.708 z m 0.3,-2 a 0.473,0.473 0 0 0 -0.154,0.354 0.4,0.4 0 0 0 0.04,0.189 0.353,0.353 0 0 0 0.11,0.161 0.469,0.469 0 0 0 0.35,0.15 0.406,0.406 0 0 0 0.19,-0.04 0.372,0.372 0 0 0 0.16,-0.11 0.454,0.454 0 0 0 0.15,-0.35 0.473,0.473 0 0 0 -0.15,-0.351 0.5,0.5 0 0 0 -0.7,0 z m -3,3 a 0.473,0.473 0 0 0 -0.154,0.354 0.454,0.454 0 0 0 0.15,0.35 0.372,0.372 0 0 0 0.16,0.11 0.406,0.406 0 0 0 0.19,0.04 0.469,0.469 0 0 0 0.35,-0.15 0.353,0.353 0 0 0 0.11,-0.161 0.4,0.4 0 0 0 0.04,-0.189 0.473,0.473 0 0 0 -0.15,-0.351 0.5,0.5 0 0 0 -0.7,0 z M 18,5.0006316 a 3,3 0 0 0 -3,-3 H 7 a 3,3 0 0 0 -3,3 v 8.0000004 a 3,3 0 0 0 3,3 h 8 a 3,3 0 0 0 3,-3 z m -2,8.0000004 a 1,1 0 0 1 -1,1 H 7 a 1,1 0 0 1 -1,-1 V 7.0006316 H 16 Z M 16,6.0006316 H 6 v -1 a 1,1 0 0 1 1,-1 h 8 a 1,1 0 0 1 1,1 z M 11,18.000632 H 3 a 1,1 0 0 1 -1,-1 v -6 h 1 v -1 H 2 V 9.0006316 a 1,1 0 0 1 1,-1 v -2 a 3,3 0 0 0 -3,3 v 8.0000004 a 3,3 0 0 0 3,3 h 8 a 3,3 0 0 0 3,-3 h -2 a 1,1 0 0 1 -1,1 z' } ],
[ 'ph-readermode-text-size', { viewBox: '0 0 20 12.5', path: 'M 10.422,11.223 A 0.712,0.712 0 0 1 10.295,11.007 L 6.581,0 H 4.68 L 0.933,11.309 0,11.447 V 12.5 H 3.594 V 11.447 L 2.655,11.325 A 0.3,0.3 0 0 1 2.468,11.211 0.214,0.214 0 0 1 2.419,10.974 L 3.341,8.387 h 3.575 l 0.906,2.652 a 0.18,0.18 0 0 1 -0.016,0.18 0.217,0.217 0 0 1 -0.139,0.106 L 6.679,11.447 V 12.5 h 4.62 V 11.447 L 10.663,11.325 A 0.512,0.512 0 0 1 10.422,11.223 Z M 3.659,7.399 5.063,2.57 6.5,7.399 Z M 19.27,11.464 A 0.406,0.406 0 0 1 19.009,11.337 0.368,0.368 0 0 1 18.902,11.072 V 6.779 A 3.838,3.838 0 0 0 18.67,5.318 1.957,1.957 0 0 0 18.01,4.457 2.48,2.48 0 0 0 16.987,4.044 7.582,7.582 0 0 0 15.67,3.938 a 6.505,6.505 0 0 0 -1.325,0.139 5.2,5.2 0 0 0 -1.2,0.4 2.732,2.732 0 0 0 -0.864,0.624 1.215,1.215 0 0 0 -0.331,0.833 0.532,0.532 0 0 0 0.119,0.383 0.665,0.665 0 0 0 0.257,0.172 0.916,0.916 0 0 0 0.375,0.041 h 1.723 V 4.942 A 4.429,4.429 0 0 1 14.611,4.91 2.045,2.045 0 0 1 14.836,4.885 c 0.09,0 0.192,-0.008 0.306,-0.008 a 1.849,1.849 0 0 1 0.808,0.151 1.247,1.247 0 0 1 0.71,0.89 2.164,2.164 0 0 1 0.049,0.51 c 0,0.076 -0.008,0.152 -0.008,0.228 0,0.076 -0.008,0.139 -0.008,0.221 v 0.2 q -1.152,0.252 -1.976,0.489 a 12.973,12.973 0 0 0 -1.391,0.474 4.514,4.514 0 0 0 -0.91,0.485 2.143,2.143 0 0 0 -0.527,0.523 1.594,1.594 0 0 0 -0.245,0.592 3.739,3.739 0 0 0 -0.061,0.693 2.261,2.261 0 0 0 0.171,0.9 2.024,2.024 0 0 0 0.469,0.682 2.084,2.084 0 0 0 0.693,0.432 2.364,2.364 0 0 0 0.852,0.151 3.587,3.587 0 0 0 1.068,-0.159 6.441,6.441 0 0 0 1.835,-0.877 l 0.22,0.832 H 20 v -0.783 z m -2.588,-0.719 a 4.314,4.314 0 0 1 -0.5,0.188 5.909,5.909 0 0 1 -0.493,0.123 2.665,2.665 0 0 1 -0.543,0.057 1.173,1.173 0 0 1 -0.861,-0.363 1.166,1.166 0 0 1 -0.245,-0.392 1.357,1.357 0 0 1 -0.086,-0.486 1.632,1.632 0 0 1 0.123,-0.657 1.215,1.215 0 0 1 0.432,-0.5 3.151,3.151 0 0 1 0.837,-0.392 12.429,12.429 0 0 1 1.334,-0.334 z' } ],
]);
return function(root) {
const icons = (root || document).querySelectorAll('.fa-icon');
if ( icons.length === 0 ) { return; }
const svgNS = 'http://www.w3.org/2000/svg';
for ( const icon of icons ) {
if ( icon.firstChild === null || icon.firstChild.nodeType !== 3 ) {
continue;
}
const name = icon.firstChild.nodeValue.trim();
if ( name === '' ) { continue; }
const svg = document.createElementNS(svgNS, 'svg');
svg.classList.add('fa-icon_' + name);
const details = svgIcons.get(name);
if ( details === undefined ) {
let file;
if ( name.startsWith('ph-') ) {
file = 'photon';
} else if ( name.startsWith('md-') ) {
file = 'material-design';
} else {
continue;
}
const use = document.createElementNS(svgNS, 'use');
use.setAttribute('href', `/img/${file}.svg#${name}`);
svg.appendChild(use);
} else {
svg.setAttribute('viewBox', details.viewBox);
const path = document.createElementNS(svgNS, 'path');
path.setAttribute('d', details.path);
svg.appendChild(path);
}
icon.replaceChild(svg, icon.firstChild);
if ( icon.classList.contains('fa-icon-badged') ) {
const badge = document.createElement('span');
badge.className = 'fa-icon-badge';
icon.insertBefore(badge, icon.firstChild.nextSibling);
}
}
};
})();
faIconsInit();

View File

@@ -0,0 +1,514 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2018-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
*/
import {
domainFromHostname,
hostnameFromURI,
originFromURI,
} from './uri-utils.js';
/******************************************************************************/
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/ResourceType
// Long term, convert code wherever possible to work with integer-based type
// values -- the assumption being that integer operations are faster than
// string operations.
export const NO_TYPE = 0;
export const BEACON = 1 << 0;
export const CSP_REPORT = 1 << 1;
export const FONT = 1 << 2;
export const IMAGE = 1 << 4;
export const IMAGESET = 1 << 4;
export const MAIN_FRAME = 1 << 5;
export const MEDIA = 1 << 6;
export const OBJECT = 1 << 7;
export const OBJECT_SUBREQUEST = 1 << 7;
export const PING = 1 << 8;
export const SCRIPT = 1 << 9;
export const STYLESHEET = 1 << 10;
export const SUB_FRAME = 1 << 11;
export const WEBSOCKET = 1 << 12;
export const XMLHTTPREQUEST = 1 << 13;
export const INLINE_FONT = 1 << 14;
export const INLINE_SCRIPT = 1 << 15;
export const OTHER = 1 << 16;
export const FRAME_ANY = MAIN_FRAME | SUB_FRAME | OBJECT;
export const FONT_ANY = FONT | INLINE_FONT;
export const INLINE_ANY = INLINE_FONT | INLINE_SCRIPT;
export const PING_ANY = BEACON | CSP_REPORT | PING;
export const SCRIPT_ANY = SCRIPT | INLINE_SCRIPT;
const typeStrToIntMap = {
'no_type': NO_TYPE,
'beacon': BEACON,
'csp_report': CSP_REPORT,
'font': FONT,
'image': IMAGE,
'imageset': IMAGESET,
'main_frame': MAIN_FRAME,
'media': MEDIA,
'object': OBJECT,
'object_subrequest': OBJECT_SUBREQUEST,
'ping': PING,
'script': SCRIPT,
'stylesheet': STYLESHEET,
'sub_frame': SUB_FRAME,
'websocket': WEBSOCKET,
'xmlhttprequest': XMLHTTPREQUEST,
'inline-font': INLINE_FONT,
'inline-script': INLINE_SCRIPT,
'other': OTHER,
};
export const METHOD_NONE = 0;
export const METHOD_CONNECT = 1 << 1;
export const METHOD_DELETE = 1 << 2;
export const METHOD_GET = 1 << 3;
export const METHOD_HEAD = 1 << 4;
export const METHOD_OPTIONS = 1 << 5;
export const METHOD_PATCH = 1 << 6;
export const METHOD_POST = 1 << 7;
export const METHOD_PUT = 1 << 8;
const methodStrToBitMap = {
'': METHOD_NONE,
'connect': METHOD_CONNECT,
'delete': METHOD_DELETE,
'get': METHOD_GET,
'head': METHOD_HEAD,
'options': METHOD_OPTIONS,
'patch': METHOD_PATCH,
'post': METHOD_POST,
'put': METHOD_PUT,
'CONNECT': METHOD_CONNECT,
'DELETE': METHOD_DELETE,
'GET': METHOD_GET,
'HEAD': METHOD_HEAD,
'OPTIONS': METHOD_OPTIONS,
'PATCH': METHOD_PATCH,
'POST': METHOD_POST,
'PUT': METHOD_PUT,
};
const methodBitToStrMap = new Map([
[ METHOD_NONE, '' ],
[ METHOD_CONNECT, 'connect' ],
[ METHOD_DELETE, 'delete' ],
[ METHOD_GET, 'get' ],
[ METHOD_HEAD, 'head' ],
[ METHOD_OPTIONS, 'options' ],
[ METHOD_PATCH, 'patch' ],
[ METHOD_POST, 'post' ],
[ METHOD_PUT, 'put' ],
]);
const reIPv4 = /^\d+\.\d+\.\d+\.\d+$/;
/******************************************************************************/
export const FilteringContext = class {
constructor(other) {
if ( other instanceof FilteringContext ) {
return this.fromFilteringContext(other);
}
this.tstamp = 0;
this.realm = '';
this.method = 0;
this.itype = NO_TYPE;
this.stype = undefined;
this.url = undefined;
this.aliasURL = undefined;
this.hostname = undefined;
this.domain = undefined;
this.ipaddress = undefined;
this.docId = -1;
this.frameId = -1;
this.docOrigin = undefined;
this.docHostname = undefined;
this.docDomain = undefined;
this.tabId = undefined;
this.tabOrigin = undefined;
this.tabHostname = undefined;
this.tabDomain = undefined;
this.redirectURL = undefined;
this.filter = undefined;
}
get type() {
return this.stype;
}
set type(a) {
this.itype = typeStrToIntMap[a] || NO_TYPE;
this.stype = a;
}
isRootDocument() {
return (this.itype & MAIN_FRAME) !== 0;
}
isDocument() {
return (this.itype & FRAME_ANY) !== 0;
}
isFont() {
return (this.itype & FONT_ANY) !== 0;
}
fromFilteringContext(other) {
this.realm = other.realm;
this.type = other.type;
this.method = other.method;
this.url = other.url;
this.hostname = other.hostname;
this.domain = other.domain;
this.ipaddress = other.ipaddress;
this.docId = other.docId;
this.frameId = other.frameId;
this.docOrigin = other.docOrigin;
this.docHostname = other.docHostname;
this.docDomain = other.docDomain;
this.tabId = other.tabId;
this.tabOrigin = other.tabOrigin;
this.tabHostname = other.tabHostname;
this.tabDomain = other.tabDomain;
this.redirectURL = other.redirectURL;
this.filter = undefined;
return this;
}
fromDetails({ originURL, url, type }) {
this.setDocOriginFromURL(originURL)
.setURL(url)
.setType(type);
return this;
}
duplicate() {
return (new FilteringContext(this));
}
setRealm(a) {
this.realm = a;
return this;
}
setType(a) {
this.type = a;
return this;
}
setURL(a) {
if ( a !== this.url ) {
this.hostname = this.domain = this.ipaddress = undefined;
this.url = a;
}
return this;
}
getHostname() {
if ( this.hostname === undefined ) {
this.hostname = hostnameFromURI(this.url);
}
return this.hostname;
}
setHostname(a) {
if ( a !== this.hostname ) {
this.domain = undefined;
this.hostname = a;
}
return this;
}
getDomain() {
if ( this.domain === undefined ) {
this.domain = domainFromHostname(this.getHostname());
}
return this.domain;
}
setDomain(a) {
this.domain = a;
return this;
}
getIPAddress() {
if ( this.ipaddress !== undefined ) {
return this.ipaddress;
}
const ipaddr = this.getHostname();
const c0 = ipaddr.charCodeAt(0);
if ( c0 === 0x5B /* [ */ ) {
return (this.ipaddress = ipaddr.slice(1, -1));
} else if ( c0 <= 0x39 && c0 >= 0x30 ) {
if ( reIPv4.test(ipaddr) ) {
return (this.ipaddress = ipaddr);
}
}
return (this.ipaddress = '');
}
// Must always be called *after* setURL()
setIPAddress(ipaddr) {
this.ipaddress = ipaddr || undefined;
return this;
}
getDocOrigin() {
if ( this.docOrigin === undefined ) {
this.docOrigin = this.tabOrigin;
}
return this.docOrigin;
}
setDocOrigin(a) {
if ( a !== this.docOrigin ) {
this.docHostname = this.docDomain = undefined;
this.docOrigin = a;
}
return this;
}
setDocOriginFromURL(a) {
return this.setDocOrigin(originFromURI(a));
}
getDocHostname() {
if ( this.docHostname === undefined ) {
this.docHostname = hostnameFromURI(this.getDocOrigin());
}
return this.docHostname;
}
setDocHostname(a) {
if ( a !== this.docHostname ) {
this.docDomain = undefined;
this.docHostname = a;
}
return this;
}
getDocDomain() {
if ( this.docDomain === undefined ) {
this.docDomain = domainFromHostname(this.getDocHostname());
}
return this.docDomain;
}
setDocDomain(a) {
this.docDomain = a;
return this;
}
// The idea is to minimize the amount of work done to figure out whether
// the resource is 3rd-party to the document.
is3rdPartyToDoc() {
let docDomain = this.getDocDomain();
if ( docDomain === '' ) { docDomain = this.docHostname; }
if ( this.domain !== undefined && this.domain !== '' ) {
return this.domain !== docDomain;
}
const hostname = this.getHostname();
if ( hostname.endsWith(docDomain) === false ) { return true; }
const i = hostname.length - docDomain.length;
if ( i === 0 ) { return false; }
return hostname.charCodeAt(i - 1) !== 0x2E /* '.' */;
}
setTabId(a) {
this.tabId = a;
return this;
}
getTabOrigin() {
return this.tabOrigin;
}
setTabOrigin(a) {
if ( a !== this.tabOrigin ) {
this.tabHostname = this.tabDomain = undefined;
this.tabOrigin = a;
}
return this;
}
setTabOriginFromURL(a) {
return this.setTabOrigin(originFromURI(a));
}
getTabHostname() {
if ( this.tabHostname === undefined ) {
this.tabHostname = hostnameFromURI(this.getTabOrigin());
}
return this.tabHostname;
}
setTabHostname(a) {
if ( a !== this.tabHostname ) {
this.tabDomain = undefined;
this.tabHostname = a;
}
return this;
}
getTabDomain() {
if ( this.tabDomain === undefined ) {
this.tabDomain = domainFromHostname(this.getTabHostname());
}
return this.tabDomain;
}
setTabDomain(a) {
this.docDomain = a;
return this;
}
// The idea is to minimize the amount of work done to figure out whether
// the resource is 3rd-party to the top document.
is3rdPartyToTab() {
let tabDomain = this.getTabDomain();
if ( tabDomain === '' ) { tabDomain = this.tabHostname; }
if ( this.domain !== undefined && this.domain !== '' ) {
return this.domain !== tabDomain;
}
const hostname = this.getHostname();
if ( hostname.endsWith(tabDomain) === false ) { return true; }
const i = hostname.length - tabDomain.length;
if ( i === 0 ) { return false; }
return hostname.charCodeAt(i - 1) !== 0x2E /* '.' */;
}
setFilter(a) {
this.filter = a;
return this;
}
pushFilter(a) {
if ( this.filter === undefined ) {
return this.setFilter(a);
}
if ( Array.isArray(this.filter) ) {
this.filter.push(a);
} else {
this.filter = [ this.filter, a ];
}
return this;
}
pushFilters(a) {
if ( this.filter === undefined ) {
return this.setFilter(a);
}
if ( Array.isArray(this.filter) ) {
this.filter.push(...a);
} else {
this.filter = [ this.filter, ...a ];
}
return this;
}
setMethod(a) {
this.method = methodStrToBitMap[a] || 0;
return this;
}
getMethodName() {
return FilteringContext.getMethodName(this.method);
}
static getMethod(a) {
return methodStrToBitMap[a] || 0;
}
static getMethodName(a) {
return methodBitToStrMap.get(a) || '';
}
BEACON = BEACON;
CSP_REPORT = CSP_REPORT;
FONT = FONT;
IMAGE = IMAGE;
IMAGESET = IMAGESET;
MAIN_FRAME = MAIN_FRAME;
MEDIA = MEDIA;
OBJECT = OBJECT;
OBJECT_SUBREQUEST = OBJECT_SUBREQUEST;
PING = PING;
SCRIPT = SCRIPT;
STYLESHEET = STYLESHEET;
SUB_FRAME = SUB_FRAME;
WEBSOCKET = WEBSOCKET;
XMLHTTPREQUEST = XMLHTTPREQUEST;
INLINE_FONT = INLINE_FONT;
INLINE_SCRIPT = INLINE_SCRIPT;
OTHER = OTHER;
FRAME_ANY = FRAME_ANY;
FONT_ANY = FONT_ANY;
INLINE_ANY = INLINE_ANY;
PING_ANY = PING_ANY;
SCRIPT_ANY = SCRIPT_ANY;
METHOD_NONE = METHOD_NONE;
METHOD_CONNECT = METHOD_CONNECT;
METHOD_DELETE = METHOD_DELETE;
METHOD_GET = METHOD_GET;
METHOD_HEAD = METHOD_HEAD;
METHOD_OPTIONS = METHOD_OPTIONS;
METHOD_PATCH = METHOD_PATCH;
METHOD_POST = METHOD_POST;
METHOD_PUT = METHOD_PUT;
static BEACON = BEACON;
static CSP_REPORT = CSP_REPORT;
static FONT = FONT;
static IMAGE = IMAGE;
static IMAGESET = IMAGESET;
static MAIN_FRAME = MAIN_FRAME;
static MEDIA = MEDIA;
static OBJECT = OBJECT;
static OBJECT_SUBREQUEST = OBJECT_SUBREQUEST;
static PING = PING;
static SCRIPT = SCRIPT;
static STYLESHEET = STYLESHEET;
static SUB_FRAME = SUB_FRAME;
static WEBSOCKET = WEBSOCKET;
static XMLHTTPREQUEST = XMLHTTPREQUEST;
static INLINE_FONT = INLINE_FONT;
static INLINE_SCRIPT = INLINE_SCRIPT;
static OTHER = OTHER;
static FRAME_ANY = FRAME_ANY;
static FONT_ANY = FONT_ANY;
static INLINE_ANY = INLINE_ANY;
static PING_ANY = PING_ANY;
static SCRIPT_ANY = SCRIPT_ANY;
static METHOD_NONE = METHOD_NONE;
static METHOD_CONNECT = METHOD_CONNECT;
static METHOD_DELETE = METHOD_DELETE;
static METHOD_GET = METHOD_GET;
static METHOD_HEAD = METHOD_HEAD;
static METHOD_OPTIONS = METHOD_OPTIONS;
static METHOD_PATCH = METHOD_PATCH;
static METHOD_POST = METHOD_POST;
static METHOD_PUT = METHOD_PUT;
};
/******************************************************************************/

View File

@@ -0,0 +1,50 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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';
/******************************************************************************/
import DynamicHostRuleFiltering from './dynamic-net-filtering.js';
import DynamicSwitchRuleFiltering from './hnswitches.js';
import DynamicURLRuleFiltering from './url-net-filtering.js';
/******************************************************************************/
const permanentFirewall = new DynamicHostRuleFiltering();
const sessionFirewall = new DynamicHostRuleFiltering();
const permanentURLFiltering = new DynamicURLRuleFiltering();
const sessionURLFiltering = new DynamicURLRuleFiltering();
const permanentSwitches = new DynamicSwitchRuleFiltering();
const sessionSwitches = new DynamicSwitchRuleFiltering();
/******************************************************************************/
export {
permanentFirewall,
sessionFirewall,
permanentURLFiltering,
sessionURLFiltering,
permanentSwitches,
sessionSwitches,
};

View File

@@ -0,0 +1,289 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2015-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
*/
/* jshint bitwise: false */
'use strict';
/******************************************************************************/
import punycode from '../lib/punycode.js';
import { decomposeHostname } from './uri-utils.js';
import { LineIterator } from './text-utils.js';
/******************************************************************************/
const decomposedSource = [];
// Object.create(null) is used below to eliminate worries about unexpected
// property names in prototype chain -- and this way we don't have to use
// hasOwnProperty() to avoid this.
const switchBitOffsets = Object.create(null);
Object.assign(switchBitOffsets, {
'no-strict-blocking': 0,
'no-popups': 2,
'no-cosmetic-filtering': 4,
'no-remote-fonts': 6,
'no-large-media': 8,
'no-csp-reports': 10,
'no-scripting': 12,
});
const switchStateToNameMap = Object.create(null);
Object.assign(switchStateToNameMap, {
'1': 'true',
'2': 'false',
});
const nameToSwitchStateMap = Object.create(null);
Object.assign(nameToSwitchStateMap, {
'true': 1,
'false': 2,
'on': 1,
'off': 2,
});
/******************************************************************************/
// For performance purpose, as simple test as possible
const reNotASCII = /[^\x20-\x7F]/;
// http://tools.ietf.org/html/rfc5952
// 4.3: "MUST be represented in lowercase"
// Also: http://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_network_resource_identifiers
/******************************************************************************/
class DynamicSwitchRuleFiltering {
constructor() {
this.reset();
}
reset() {
this.switches = new Map();
this.n = '';
this.z = '';
this.r = 0;
this.changed = true;
}
assign(from) {
// Remove rules not in other
for ( const hn of this.switches.keys() ) {
if ( from.switches.has(hn) === false ) {
this.switches.delete(hn);
this.changed = true;
}
}
// Add/change rules in other
for ( const [hn, bits] of from.switches ) {
if ( this.switches.get(hn) !== bits ) {
this.switches.set(hn, bits);
this.changed = true;
}
}
}
copyRules(from, srcHostname) {
const thisBits = this.switches.get(srcHostname);
const fromBits = from.switches.get(srcHostname);
if ( fromBits !== thisBits ) {
if ( fromBits !== undefined ) {
this.switches.set(srcHostname, fromBits);
} else {
this.switches.delete(srcHostname);
}
this.changed = true;
}
return this.changed;
}
hasSameRules(other, srcHostname) {
return this.switches.get(srcHostname) === other.switches.get(srcHostname);
}
toggle(switchName, hostname, newVal) {
const bitOffset = switchBitOffsets[switchName];
if ( bitOffset === undefined ) { return false; }
if ( newVal === this.evaluate(switchName, hostname) ) { return false; }
let bits = this.switches.get(hostname) || 0;
bits &= ~(3 << bitOffset);
bits |= newVal << bitOffset;
if ( bits === 0 ) {
this.switches.delete(hostname);
} else {
this.switches.set(hostname, bits);
}
this.changed = true;
return true;
}
toggleOneZ(switchName, hostname, newState) {
const bitOffset = switchBitOffsets[switchName];
if ( bitOffset === undefined ) { return false; }
let state = this.evaluateZ(switchName, hostname);
if ( newState === state ) { return false; }
if ( newState === undefined ) {
newState = !state;
}
let bits = this.switches.get(hostname) || 0;
bits &= ~(3 << bitOffset);
if ( bits === 0 ) {
this.switches.delete(hostname);
} else {
this.switches.set(hostname, bits);
}
state = this.evaluateZ(switchName, hostname);
if ( state !== newState ) {
this.switches.set(hostname, bits | ((newState ? 1 : 2) << bitOffset));
}
this.changed = true;
return true;
}
toggleBranchZ(switchName, targetHostname, newState) {
this.toggleOneZ(switchName, targetHostname, newState);
// Turn off all descendant switches, they will inherit the state of the
// branch's origin.
const targetLen = targetHostname.length;
for ( const hostname of this.switches.keys() ) {
if ( hostname === targetHostname ) { continue; }
if ( hostname.length <= targetLen ) { continue; }
if ( hostname.endsWith(targetHostname) === false ) { continue; }
if ( hostname.charAt(hostname.length - targetLen - 1) !== '.' ) {
continue;
}
this.toggle(switchName, hostname, 0);
}
return this.changed;
}
toggleZ(switchName, hostname, deep, newState) {
if ( deep === true ) {
return this.toggleBranchZ(switchName, hostname, newState);
}
return this.toggleOneZ(switchName, hostname, newState);
}
// 0 = inherit from broader scope, up to default state
// 1 = non-default state
// 2 = forced default state (to override a broader non-default state)
evaluate(switchName, hostname) {
const bits = this.switches.get(hostname);
if ( bits === undefined ) { return 0; }
let bitOffset = switchBitOffsets[switchName];
if ( bitOffset === undefined ) { return 0; }
return (bits >>> bitOffset) & 3;
}
evaluateZ(switchName, hostname) {
const bitOffset = switchBitOffsets[switchName];
if ( bitOffset === undefined ) {
this.r = 0;
return false;
}
this.n = switchName;
for ( const shn of decomposeHostname(hostname, decomposedSource) ) {
let bits = this.switches.get(shn);
if ( bits === undefined ) { continue; }
bits = bits >>> bitOffset & 3;
if ( bits === 0 ) { continue; }
this.z = shn;
this.r = bits;
return bits === 1;
}
this.r = 0;
return false;
}
toLogData() {
return {
source: 'switch',
result: this.r,
raw: `${this.n}: ${this.z} true`
};
}
toArray() {
const out = [];
for ( const hostname of this.switches.keys() ) {
const prettyHn = hostname.includes('xn--') && punycode
? punycode.toUnicode(hostname)
: hostname;
for ( const switchName in switchBitOffsets ) {
if ( switchBitOffsets[switchName] === undefined ) { continue; }
const val = this.evaluate(switchName, hostname);
if ( val === 0 ) { continue; }
out.push(`${switchName}: ${prettyHn} ${switchStateToNameMap[val]}`);
}
}
return out;
}
toString() {
return this.toArray().join('\n');
}
fromString(text, append) {
const lineIter = new LineIterator(text);
if ( append !== true ) { this.reset(); }
while ( lineIter.eot() === false ) {
this.addFromRuleParts(lineIter.next().trim().split(/\s+/));
}
}
validateRuleParts(parts) {
if ( parts.length < 3 ) { return; }
if ( parts[0].endsWith(':') === false ) { return; }
if ( nameToSwitchStateMap[parts[2]] === undefined ) { return; }
if ( reNotASCII.test(parts[1]) && punycode !== undefined ) {
parts[1] = punycode.toASCII(parts[1]);
}
return parts;
}
addFromRuleParts(parts) {
if ( this.validateRuleParts(parts) === undefined ) { return false; }
const switchName = parts[0].slice(0, -1);
if ( switchBitOffsets[switchName] === undefined ) { return false; }
this.toggle(switchName, parts[1], nameToSwitchStateMap[parts[2]]);
return true;
}
removeFromRuleParts(parts) {
if ( this.validateRuleParts(parts) !== undefined ) {
this.toggle(parts[0].slice(0, -1), parts[1], 0);
return true;
}
return false;
}
}
/******************************************************************************/
export default DynamicSwitchRuleFiltering;
/******************************************************************************/

View File

@@ -0,0 +1,771 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2017-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
*/
/*******************************************************************************
The original prototype was to develop an idea I had about using jump indices
in a TypedArray for quickly matching hostnames (or more generally strings)[1].
Once I had a working, un-optimized prototype, I realized I had ended up
with something formally named a "trie": <https://en.wikipedia.org/wiki/Trie>,
hence the name. I have no idea whether the implementation here or one
resembling it has been done elsewhere.
"HN" in HNTrieContainer stands for "HostName", because the trie is
specialized to deal with matching hostnames -- which is a bit more
complicated than matching plain strings.
For example, `www.abc.com` is deemed matching `abc.com`, because the former
is a subdomain of the latter. The opposite is of course not true.
The resulting read-only tries created as a result of using HNTrieContainer
are simply just typed arrays filled with integers. The matching algorithm is
just a matter of reading/comparing these integers, and further using them as
indices in the array as a way to move around in the trie.
[1] To solve <https://github.com/gorhill/uBlock/issues/3193>
Since this trie is specialized for matching hostnames, the stored
strings are reversed internally, because of hostname comparison logic:
Correct matching:
index 0123456
abc.com
|
www.abc.com
index 01234567890
Incorrect matching (typically used for plain strings):
index 0123456
abc.com
|
www.abc.com
index 01234567890
------------------------------------------------------------------------------
1st iteration:
- https://github.com/gorhill/uBlock/blob/ff58107dac3a32607f8113e39ed5015584506813/src/js/hntrie.js
- Suitable for small to medium set of hostnames
- One buffer per trie
2nd iteration: goal was to make matches() method wasm-able
- https://github.com/gorhill/uBlock/blob/c3b0fd31f64bd7ffecdd282fb1208fe07aac3eb0/src/js/hntrie.js
- Suitable for small to medium set of hostnames
- Distinct tries all share same buffer:
- Reduced memory footprint
- https://stackoverflow.com/questions/45803829/memory-overhead-of-typed-arrays-vs-strings/45808835#45808835
- Reusing needle character lookups for all tries
- This significantly reduce the number of String.charCodeAt() calls
- Slightly improved creation time
This is the 3rd iteration: goal was to make add() method wasm-able and
further improve memory/CPU efficiency.
This 3rd iteration has the following new traits:
- Suitable for small to large set of hostnames
- Support multiple trie containers (instanciable)
- Designed to hold large number of hostnames
- Hostnames can be added at any time (instead of all at once)
- This means pre-sorting is no longer a requirement
- The trie is always compact
- There is no longer a need for a `vacuum` method
- This makes the add() method wasm-able
- It can return the exact hostname which caused the match
- serializable/unserializable available for fast loading
- Distinct trie reference support the iteration protocol, thus allowing
to extract all the hostnames in the trie
Its primary purpose is to replace the use of Set() as a mean to hold
large number of hostnames (ex. FilterHostnameDict in static filtering
engine).
A HNTrieContainer is mostly a large buffer in which distinct but related
tries are stored. The memory layout of the buffer is as follow:
0-254: needle being processed
255: length of needle
256-259: offset to start of trie data section (=> trie0)
260-263: offset to end of trie data section (=> trie1)
264-267: offset to start of character data section (=> char0)
268-271: offset to end of character data section (=> char1)
272: start of trie data section
*/
const PAGE_SIZE = 65536;
// i32 / i8
const TRIE0_SLOT = 256 >>> 2; // 64 / 256
const TRIE1_SLOT = TRIE0_SLOT + 1; // 65 / 260
const CHAR0_SLOT = TRIE0_SLOT + 2; // 66 / 264
const CHAR1_SLOT = TRIE0_SLOT + 3; // 67 / 268
const TRIE0_START = TRIE0_SLOT + 4 << 2; // 272
const roundToPageSize = v => (v + PAGE_SIZE-1) & ~(PAGE_SIZE-1);
// http://www.cse.yorku.ca/~oz/hash.html#djb2
const i32Checksum = (buf32) => {
const n = buf32.length;
let hash = 177573 ^ n;
for ( let i = 0; i < n; i++ ) {
hash = (hash << 5) + hash ^ buf32[i];
}
return hash;
};
class HNTrieContainer {
constructor() {
const len = PAGE_SIZE * 2;
this.buf = new Uint8Array(len);
this.buf32 = new Uint32Array(this.buf.buffer);
this.needle = '';
this.buf32[TRIE0_SLOT] = TRIE0_START;
this.buf32[TRIE1_SLOT] = this.buf32[TRIE0_SLOT];
this.buf32[CHAR0_SLOT] = len >>> 1;
this.buf32[CHAR1_SLOT] = this.buf32[CHAR0_SLOT];
this.wasmMemory = null;
this.lastStored = '';
this.lastStoredLen = this.lastStoredIndex = 0;
}
//--------------------------------------------------------------------------
// Public methods
//--------------------------------------------------------------------------
reset(details) {
if (
details instanceof Object &&
typeof details.byteLength === 'number' &&
typeof details.char0 === 'number'
) {
if ( details.byteLength > this.buf.byteLength ) {
this.reallocateBuf(details.byteLength);
}
this.buf32[CHAR0_SLOT] = details.char0;
}
this.buf32[TRIE1_SLOT] = this.buf32[TRIE0_SLOT];
this.buf32[CHAR1_SLOT] = this.buf32[CHAR0_SLOT];
this.lastStored = '';
this.lastStoredLen = this.lastStoredIndex = 0;
}
setNeedle(needle) {
if ( needle !== this.needle ) {
const buf = this.buf;
let i = needle.length;
if ( i > 255 ) { i = 255; }
buf[255] = i;
while ( i-- ) {
buf[i] = needle.charCodeAt(i);
}
this.needle = needle;
}
return this;
}
matchesJS(iroot) {
const buf32 = this.buf32;
const buf8 = this.buf;
const char0 = buf32[CHAR0_SLOT];
let ineedle = buf8[255];
let icell = buf32[iroot+0];
if ( icell === 0 ) { return -1; }
let c = 0, v = 0, i0 = 0, n = 0;
for (;;) {
if ( ineedle === 0 ) { return -1; }
ineedle -= 1;
c = buf8[ineedle];
// find first segment with a first-character match
for (;;) {
v = buf32[icell+2];
i0 = char0 + (v >>> 8);
if ( buf8[i0] === c ) { break; }
icell = buf32[icell+0];
if ( icell === 0 ) { return -1; }
}
// all characters in segment must match
n = v & 0x7F;
if ( n > 1 ) {
n -= 1;
if ( n > ineedle ) { return -1; }
i0 += 1;
const i1 = i0 + n;
do {
ineedle -= 1;
if ( buf8[i0] !== buf8[ineedle] ) { return -1; }
i0 += 1;
} while ( i0 < i1 );
}
// boundary at end of segment?
if ( (v & 0x80) !== 0 ) {
if ( ineedle === 0 || buf8[ineedle-1] === 0x2E /* '.' */ ) {
return ineedle;
}
}
// next segment
icell = buf32[icell+1];
if ( icell === 0 ) { break; }
}
return -1;
}
createTrie() {
// grow buffer if needed
if ( (this.buf32[CHAR0_SLOT] - this.buf32[TRIE1_SLOT]) < 12 ) {
this.growBuf(12, 0);
}
const iroot = this.buf32[TRIE1_SLOT] >>> 2;
this.buf32[TRIE1_SLOT] += 12;
this.buf32[iroot+0] = 0;
this.buf32[iroot+1] = 0;
this.buf32[iroot+2] = 0;
return iroot;
}
createTrieFromIterable(hostnames) {
const itrie = this.createTrie();
for ( const hn of hostnames ) {
if ( hn === '' ) { continue; }
this.setNeedle(hn).add(itrie);
}
return itrie;
}
createTrieFromStoredDomainOpt(i, n) {
const itrie = this.createTrie();
const jend = i + n;
let j = i, offset = 0, k = 0, c = 0;
while ( j !== jend ) {
offset = this.buf32[CHAR0_SLOT]; // Important
k = 0;
for (;;) {
if ( j === jend ) { break; }
c = this.buf[offset+j];
j += 1;
if ( c === 0x7C /* '|' */ ) { break; }
if ( k === 255 ) { continue; }
this.buf[k] = c;
k += 1;
}
if ( k !== 0 ) {
this.buf[255] = k;
this.add(itrie);
}
}
this.needle = ''; // Important
this.buf[255] = 0; // Important
return itrie;
}
dumpTrie(iroot) {
let hostnames = Array.from(this.trieIterator(iroot));
if ( String.prototype.padStart instanceof Function ) {
const maxlen = Math.min(
hostnames.reduce((maxlen, hn) => Math.max(maxlen, hn.length), 0),
64
);
hostnames = hostnames.map(hn => hn.padStart(maxlen));
}
for ( const hn of hostnames ) {
console.log(hn);
}
}
trieIterator(iroot) {
return {
value: undefined,
done: false,
next() {
if ( this.icell === 0 ) {
if ( this.forks.length === 0 ) {
this.value = undefined;
this.done = true;
return this;
}
this.charPtr = this.forks.pop();
this.icell = this.forks.pop();
}
for (;;) {
const idown = this.container.buf32[this.icell+0];
if ( idown !== 0 ) {
this.forks.push(idown, this.charPtr);
}
const v = this.container.buf32[this.icell+2];
let i0 = this.container.buf32[CHAR0_SLOT] + (v >>> 8);
const i1 = i0 + (v & 0x7F);
while ( i0 < i1 ) {
this.charPtr -= 1;
this.charBuf[this.charPtr] = this.container.buf[i0];
i0 += 1;
}
this.icell = this.container.buf32[this.icell+1];
if ( (v & 0x80) !== 0 ) {
return this.toHostname();
}
}
},
toHostname() {
this.value = this.textDecoder.decode(
new Uint8Array(this.charBuf.buffer, this.charPtr)
);
return this;
},
container: this,
icell: this.buf32[iroot],
charBuf: new Uint8Array(256),
charPtr: 256,
forks: [],
textDecoder: new TextDecoder(),
[Symbol.iterator]() { return this; },
};
}
// TODO:
// Rework code to add from a string already present in the character
// buffer, i.e. not having to go through setNeedle() when adding a new
// hostname to a trie. This will require much work though, and probably
// changing the order in which string segments are stored in the
// character buffer.
addJS(iroot) {
let lhnchar = this.buf[255];
if ( lhnchar === 0 ) { return 0; }
// grow buffer if needed
if (
(this.buf32[CHAR0_SLOT] - this.buf32[TRIE1_SLOT]) < 24 ||
(this.buf.length - this.buf32[CHAR1_SLOT]) < 256
) {
this.growBuf(24, 256);
}
let icell = this.buf32[iroot+0];
// special case: first node in trie
if ( icell === 0 ) {
this.buf32[iroot+0] = this.addLeafCell(lhnchar);
return 1;
}
//
const char0 = this.buf32[CHAR0_SLOT];
let isegchar, lsegchar, boundaryBit, inext;
// find a matching cell: move down
for (;;) {
const v = this.buf32[icell+2];
let isegchar0 = char0 + (v >>> 8);
// if first character is no match, move to next descendant
if ( this.buf[isegchar0] !== this.buf[lhnchar-1] ) {
inext = this.buf32[icell+0];
if ( inext === 0 ) {
this.buf32[icell+0] = this.addLeafCell(lhnchar);
return 1;
}
icell = inext;
continue;
}
// 1st character was tested
isegchar = 1;
lhnchar -= 1;
// find 1st mismatch in rest of segment
lsegchar = v & 0x7F;
if ( lsegchar !== 1 ) {
for (;;) {
if ( isegchar === lsegchar ) { break; }
if ( lhnchar === 0 ) { break; }
if ( this.buf[isegchar0+isegchar] !== this.buf[lhnchar-1] ) { break; }
isegchar += 1;
lhnchar -= 1;
}
}
boundaryBit = v & 0x80;
// all segment characters matched
if ( isegchar === lsegchar ) {
// needle remainder: no
if ( lhnchar === 0 ) {
// boundary: yes, already present
if ( boundaryBit !== 0 ) { return 0; }
// boundary: no, mark as boundary
this.buf32[icell+2] = v | 0x80;
}
// needle remainder: yes
else {
// remainder is at label boundary? if yes, no need to add
// the rest since the shortest match is always reported
if ( boundaryBit !== 0 ) {
if ( this.buf[lhnchar-1] === 0x2E /* '.' */ ) { return -1; }
}
inext = this.buf32[icell+1];
if ( inext !== 0 ) {
icell = inext;
continue;
}
// add needle remainder
this.buf32[icell+1] = this.addLeafCell(lhnchar);
}
}
// some segment characters matched
else {
// split current cell
isegchar0 -= char0;
this.buf32[icell+2] = isegchar0 << 8 | isegchar;
inext = this.addCell(
0,
this.buf32[icell+1],
isegchar0 + isegchar << 8 | boundaryBit | lsegchar - isegchar
);
this.buf32[icell+1] = inext;
// needle remainder: yes, need new cell for remaining characters
if ( lhnchar !== 0 ) {
this.buf32[inext+0] = this.addLeafCell(lhnchar);
}
// needle remainder: no, need boundary cell
else {
this.buf32[icell+2] |= 0x80;
}
}
return 1;
}
}
optimize() {
this.shrinkBuf();
return {
byteLength: this.buf.byteLength,
char0: this.buf32[CHAR0_SLOT],
};
}
toSelfie() {
const buf32 = this.buf32.subarray(0, this.buf32[CHAR1_SLOT] + 3 >>> 2);
return { buf32, checksum: i32Checksum(buf32) };
}
fromSelfie(selfie) {
if ( typeof selfie !== 'object' || selfie === null ) { return false; }
if ( selfie.buf32 instanceof Uint32Array === false ) { return false; }
if ( selfie.checksum !== i32Checksum(selfie.buf32) ) { return false; }
this.needle = '';
let byteLength = selfie.buf32.length << 2;
if ( byteLength === 0 ) { return false; }
byteLength = roundToPageSize(byteLength);
if ( this.wasmMemory !== null ) {
const pageCountBefore = this.buf.length >>> 16;
const pageCountAfter = byteLength >>> 16;
if ( pageCountAfter > pageCountBefore ) {
this.wasmMemory.grow(pageCountAfter - pageCountBefore);
this.buf = new Uint8Array(this.wasmMemory.buffer);
this.buf32 = new Uint32Array(this.buf.buffer);
}
this.buf32.set(selfie.buf32);
} else {
this.buf32 = selfie.buf32;
this.buf = new Uint8Array(this.buf32.buffer);
}
// https://github.com/uBlockOrigin/uBlock-issues/issues/2925
this.buf[255] = 0;
return true;
}
// The following *Hostname() methods can be used to store hostname strings
// outside the trie. This is useful to store/match hostnames which are
// not part of a collection, and yet still benefit from storing the strings
// into a trie container's character buffer.
// TODO: WASM version of matchesHostname()
storeHostname(hn) {
let n = hn.length;
if ( n > 255 ) {
hn = hn.slice(-255);
n = 255;
}
if ( n === this.lastStoredLen && hn === this.lastStored ) {
return this.lastStoredIndex;
}
this.lastStored = hn;
this.lastStoredLen = n;
if ( (this.buf.length - this.buf32[CHAR1_SLOT]) < n ) {
this.growBuf(0, n);
}
const offset = this.buf32[CHAR1_SLOT];
this.buf32[CHAR1_SLOT] = offset + n;
const buf8 = this.buf;
for ( let i = 0; i < n; i++ ) {
buf8[offset+i] = hn.charCodeAt(i);
}
return (this.lastStoredIndex = offset - this.buf32[CHAR0_SLOT]);
}
extractHostname(i, n) {
const textDecoder = new TextDecoder();
const offset = this.buf32[CHAR0_SLOT] + i;
return textDecoder.decode(this.buf.subarray(offset, offset + n));
}
storeDomainOpt(s) {
let n = s.length;
if ( n === this.lastStoredLen && s === this.lastStored ) {
return this.lastStoredIndex;
}
this.lastStored = s;
this.lastStoredLen = n;
if ( (this.buf.length - this.buf32[CHAR1_SLOT]) < n ) {
this.growBuf(0, n);
}
const offset = this.buf32[CHAR1_SLOT];
this.buf32[CHAR1_SLOT] = offset + n;
const buf8 = this.buf;
for ( let i = 0; i < n; i++ ) {
buf8[offset+i] = s.charCodeAt(i);
}
return (this.lastStoredIndex = offset - this.buf32[CHAR0_SLOT]);
}
extractDomainOpt(i, n) {
const textDecoder = new TextDecoder();
const offset = this.buf32[CHAR0_SLOT] + i;
return textDecoder.decode(this.buf.subarray(offset, offset + n));
}
matchesHostname(hn, i, n) {
this.setNeedle(hn);
const buf8 = this.buf;
const hr = buf8[255];
if ( n > hr ) { return false; }
const hl = hr - n;
const nl = this.buf32[CHAR0_SLOT] + i;
for ( let j = 0; j < n; j++ ) {
if ( buf8[nl+j] !== buf8[hl+j] ) { return false; }
}
return n === hr || hn.charCodeAt(hl-1) === 0x2E /* '.' */;
}
async enableWASM(wasmModuleFetcher, path) {
if ( typeof WebAssembly === 'undefined' ) { return false; }
if ( this.wasmMemory instanceof WebAssembly.Memory ) { return true; }
const module = await getWasmModule(wasmModuleFetcher, path);
if ( module instanceof WebAssembly.Module === false ) { return false; }
const memory = new WebAssembly.Memory({ initial: 2 });
const instance = await WebAssembly.instantiate(module, {
imports: {
memory,
growBuf: this.growBuf.bind(this, 24, 256)
}
});
if ( instance instanceof WebAssembly.Instance === false ) { return false; }
this.wasmMemory = memory;
const curPageCount = memory.buffer.byteLength >>> 16;
const newPageCount = roundToPageSize(this.buf.byteLength) >>> 16;
if ( newPageCount > curPageCount ) {
memory.grow(newPageCount - curPageCount);
}
const buf = new Uint8Array(memory.buffer);
buf.set(this.buf);
this.buf = buf;
this.buf32 = new Uint32Array(this.buf.buffer);
this.matches = this.matchesWASM = instance.exports.matches;
this.add = this.addWASM = instance.exports.add;
return true;
}
dumpInfo() {
return [
`Buffer size (Uint8Array): ${this.buf32[CHAR1_SLOT].toLocaleString('en')}`,
`WASM: ${this.wasmMemory === null ? 'disabled' : 'enabled'}`,
].join('\n');
}
//--------------------------------------------------------------------------
// Private methods
//--------------------------------------------------------------------------
addCell(idown, iright, v) {
let icell = this.buf32[TRIE1_SLOT];
this.buf32[TRIE1_SLOT] = icell + 12;
icell >>>= 2;
this.buf32[icell+0] = idown;
this.buf32[icell+1] = iright;
this.buf32[icell+2] = v;
return icell;
}
addLeafCell(lsegchar) {
const r = this.buf32[TRIE1_SLOT] >>> 2;
let i = r;
while ( lsegchar > 127 ) {
this.buf32[i+0] = 0;
this.buf32[i+1] = i + 3;
this.buf32[i+2] = this.addSegment(lsegchar, lsegchar - 127);
lsegchar -= 127;
i += 3;
}
this.buf32[i+0] = 0;
this.buf32[i+1] = 0;
this.buf32[i+2] = this.addSegment(lsegchar, 0) | 0x80;
this.buf32[TRIE1_SLOT] = i + 3 << 2;
return r;
}
addSegment(lsegchar, lsegend) {
if ( lsegchar === 0 ) { return 0; }
let char1 = this.buf32[CHAR1_SLOT];
const isegchar = char1 - this.buf32[CHAR0_SLOT];
let i = lsegchar;
do {
this.buf[char1++] = this.buf[--i];
} while ( i !== lsegend );
this.buf32[CHAR1_SLOT] = char1;
return isegchar << 8 | lsegchar - lsegend;
}
growBuf(trieGrow, charGrow) {
const char0 = Math.max(
roundToPageSize(this.buf32[TRIE1_SLOT] + trieGrow),
this.buf32[CHAR0_SLOT]
);
const char1 = char0 + this.buf32[CHAR1_SLOT] - this.buf32[CHAR0_SLOT];
const bufLen = Math.max(
roundToPageSize(char1 + charGrow),
this.buf.length
);
this.resizeBuf(bufLen, char0);
}
shrinkBuf() {
// Can't shrink WebAssembly.Memory
if ( this.wasmMemory !== null ) { return; }
const char0 = this.buf32[TRIE1_SLOT] + 24;
const char1 = char0 + this.buf32[CHAR1_SLOT] - this.buf32[CHAR0_SLOT];
const bufLen = char1 + 256;
this.resizeBuf(bufLen, char0);
}
resizeBuf(bufLen, char0) {
bufLen = roundToPageSize(bufLen);
if ( bufLen === this.buf.length && char0 === this.buf32[CHAR0_SLOT] ) {
return;
}
const charDataLen = this.buf32[CHAR1_SLOT] - this.buf32[CHAR0_SLOT];
if ( this.wasmMemory !== null ) {
const pageCount = (bufLen >>> 16) - (this.buf.byteLength >>> 16);
if ( pageCount > 0 ) {
this.wasmMemory.grow(pageCount);
this.buf = new Uint8Array(this.wasmMemory.buffer);
this.buf32 = new Uint32Array(this.wasmMemory.buffer);
}
} else if ( bufLen !== this.buf.length ) {
const newBuf = new Uint8Array(bufLen);
newBuf.set(
new Uint8Array(
this.buf.buffer,
0,
this.buf32[TRIE1_SLOT]
),
0
);
newBuf.set(
new Uint8Array(
this.buf.buffer,
this.buf32[CHAR0_SLOT],
charDataLen
),
char0
);
this.buf = newBuf;
this.buf32 = new Uint32Array(this.buf.buffer);
this.buf32[CHAR0_SLOT] = char0;
this.buf32[CHAR1_SLOT] = char0 + charDataLen;
}
if ( char0 !== this.buf32[CHAR0_SLOT] ) {
this.buf.set(
new Uint8Array(
this.buf.buffer,
this.buf32[CHAR0_SLOT],
charDataLen
),
char0
);
this.buf32[CHAR0_SLOT] = char0;
this.buf32[CHAR1_SLOT] = char0 + charDataLen;
}
}
reallocateBuf(newSize) {
newSize = roundToPageSize(newSize);
if ( newSize === this.buf.length ) { return; }
if ( this.wasmMemory === null ) {
const newBuf = new Uint8Array(newSize);
newBuf.set(
newBuf.length < this.buf.length
? this.buf.subarray(0, newBuf.length)
: this.buf
);
this.buf = newBuf;
} else {
const growBy =
((newSize + 0xFFFF) >>> 16) - (this.buf.length >>> 16);
if ( growBy <= 0 ) { return; }
this.wasmMemory.grow(growBy);
this.buf = new Uint8Array(this.wasmMemory.buffer);
}
this.buf32 = new Uint32Array(this.buf.buffer);
}
}
HNTrieContainer.prototype.matches = HNTrieContainer.prototype.matchesJS;
HNTrieContainer.prototype.matchesWASM = null;
HNTrieContainer.prototype.add = HNTrieContainer.prototype.addJS;
HNTrieContainer.prototype.addWASM = null;
/******************************************************************************/
// Code below is to attempt to load a WASM module which implements:
//
// - HNTrieContainer.add()
// - HNTrieContainer.matches()
//
// The WASM module is entirely optional, the JS implementations will be
// used should the WASM module be unavailable for whatever reason.
const getWasmModule = (( ) => {
let wasmModulePromise;
return async function(wasmModuleFetcher, path) {
if ( wasmModulePromise instanceof Promise ) {
return wasmModulePromise;
}
// The wasm module will work only if CPU is natively little-endian,
// as we use native uint32 array in our js code.
const uint32s = new Uint32Array(1);
const uint8s = new Uint8Array(uint32s.buffer);
uint32s[0] = 1;
if ( uint8s[0] !== 1 ) { return; }
wasmModulePromise = wasmModuleFetcher(`${path}hntrie`).catch(reason => {
console.info(reason);
});
return wasmModulePromise;
};
})();
/******************************************************************************/
export default HNTrieContainer;

View File

@@ -0,0 +1,465 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2017-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';
/******************************************************************************/
import logger from './logger.js';
import µb from './background.js';
import { sessionFirewall } from './filtering-engines.js';
import { StaticExtFilteringHostnameDB } from './static-ext-filtering-db.js';
import { entityFromDomain } from './uri-utils.js';
/******************************************************************************/
const pselectors = new Map();
const duplicates = new Set();
const filterDB = new StaticExtFilteringHostnameDB(2);
let acceptedCount = 0;
let discardedCount = 0;
let docRegister;
const htmlFilteringEngine = {
get acceptedCount() {
return acceptedCount;
},
get discardedCount() {
return discardedCount;
},
getFilterCount() {
return filterDB.size;
},
};
const regexFromString = (s, exact = false) => {
if ( s === '' ) { return /^/; }
const match = /^\/(.+)\/([i]?)$/.exec(s);
if ( match !== null ) {
return new RegExp(match[1], match[2] || undefined);
}
const reStr = s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return new RegExp(exact ? `^${reStr}$` : reStr, 'i');
};
class PSelectorVoidTask {
constructor(task) {
console.info(`[uBO] HTML filtering: :${task[0]}() operator is not supported`);
}
transpose() {
}
}
class PSelectorHasTextTask {
constructor(task) {
this.needle = regexFromString(task[1]);
}
transpose(node, output) {
if ( this.needle.test(node.textContent) ) {
output.push(node);
}
}
}
const PSelectorIfTask = class {
constructor(task) {
this.pselector = new PSelector(task[1]);
}
transpose(node, output) {
if ( this.pselector.test(node) === this.target ) {
output.push(node);
}
}
};
PSelectorIfTask.prototype.target = true;
class PSelectorIfNotTask extends PSelectorIfTask {
}
PSelectorIfNotTask.prototype.target = false;
class PSelectorMinTextLengthTask {
constructor(task) {
this.min = task[1];
}
transpose(node, output) {
if ( node.textContent.length >= this.min ) {
output.push(node);
}
}
}
class PSelectorSpathTask {
constructor(task) {
this.spath = task[1];
this.nth = /^(?:\s*[+~]|:)/.test(this.spath);
if ( this.nth ) { return; }
if ( /^\s*>/.test(this.spath) ) {
this.spath = `:scope ${this.spath.trim()}`;
}
}
transpose(node, output) {
const nodes = this.nth
? PSelectorSpathTask.qsa(node, this.spath)
: node.querySelectorAll(this.spath);
for ( const node of nodes ) {
output.push(node);
}
}
// Helper method for other operators.
static qsa(node, selector) {
const parent = node.parentElement;
if ( parent === null ) { return []; }
let pos = 1;
for (;;) {
node = node.previousElementSibling;
if ( node === null ) { break; }
pos += 1;
}
return parent.querySelectorAll(
`:scope > :nth-child(${pos})${selector}`
);
}
}
class PSelectorUpwardTask {
constructor(task) {
const arg = task[1];
if ( typeof arg === 'number' ) {
this.i = arg;
} else {
this.s = arg;
}
}
transpose(node, output) {
if ( this.s !== '' ) {
const parent = node.parentElement;
if ( parent === null ) { return; }
node = parent.closest(this.s);
if ( node === null ) { return; }
} else {
let nth = this.i;
for (;;) {
node = node.parentElement;
if ( node === null ) { return; }
nth -= 1;
if ( nth === 0 ) { break; }
}
}
output.push(node);
}
}
PSelectorUpwardTask.prototype.i = 0;
PSelectorUpwardTask.prototype.s = '';
class PSelectorXpathTask {
constructor(task) {
this.xpe = task[1];
}
transpose(node, output) {
const xpr = docRegister.evaluate(
this.xpe,
node,
null,
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
null
);
let j = xpr.snapshotLength;
while ( j-- ) {
const node = xpr.snapshotItem(j);
if ( node.nodeType === 1 ) {
output.push(node);
}
}
}
}
class PSelector {
constructor(o) {
this.raw = o.raw;
this.selector = o.selector;
this.tasks = [];
if ( !o.tasks ) { return; }
for ( const task of o.tasks ) {
const ctor = this.operatorToTaskMap.get(task[0]) || PSelectorVoidTask;
const pselector = new ctor(task);
this.tasks.push(pselector);
}
}
prime(input) {
const root = input || docRegister;
if ( this.selector === '' ) { return [ root ]; }
if ( input !== docRegister && /^ ?[>+~]/.test(this.selector) ) {
return Array.from(PSelectorSpathTask.qsa(input, this.selector));
}
return Array.from(root.querySelectorAll(this.selector));
}
exec(input) {
let nodes = this.prime(input);
for ( const task of this.tasks ) {
if ( nodes.length === 0 ) { break; }
const transposed = [];
for ( const node of nodes ) {
task.transpose(node, transposed);
}
nodes = transposed;
}
return nodes;
}
test(input) {
const nodes = this.prime(input);
for ( const node of nodes ) {
let output = [ node ];
for ( const task of this.tasks ) {
const transposed = [];
for ( const node of output ) {
task.transpose(node, transposed);
}
output = transposed;
if ( output.length === 0 ) { break; }
}
if ( output.length !== 0 ) { return true; }
}
return false;
}
}
PSelector.prototype.operatorToTaskMap = new Map([
[ 'has', PSelectorIfTask ],
[ 'has-text', PSelectorHasTextTask ],
[ 'if', PSelectorIfTask ],
[ 'if-not', PSelectorIfNotTask ],
[ 'min-text-length', PSelectorMinTextLengthTask ],
[ 'not', PSelectorIfNotTask ],
[ 'nth-ancestor', PSelectorUpwardTask ],
[ 'spath', PSelectorSpathTask ],
[ 'upward', PSelectorUpwardTask ],
[ 'xpath', PSelectorXpathTask ],
]);
function logOne(details, exception, selector) {
µb.filteringContext
.duplicate()
.fromTabId(details.tabId)
.setRealm('extended')
.setType('dom')
.setURL(details.url)
.setDocOriginFromURL(details.url)
.setFilter({
source: 'extended',
raw: `${exception === 0 ? '##' : '#@#'}^${selector}`
})
.toLogger();
}
function applyProceduralSelector(details, selector) {
let pselector = pselectors.get(selector);
if ( pselector === undefined ) {
pselector = new PSelector(JSON.parse(selector));
pselectors.set(selector, pselector);
}
const nodes = pselector.exec();
let modified = false;
for ( const node of nodes ) {
node.remove();
modified = true;
}
if ( modified && logger.enabled ) {
logOne(details, 0, pselector.raw);
}
return modified;
}
function applyCSSSelector(details, selector) {
const nodes = docRegister.querySelectorAll(selector);
let modified = false;
for ( const node of nodes ) {
node.remove();
modified = true;
}
if ( modified && logger.enabled ) {
logOne(details, 0, selector);
}
return modified;
}
function logError(writer, msg) {
logger.writeOne({
realm: 'message',
type: 'error',
text: msg.replace('{who}', writer.properties.get('name') || '?')
});
}
htmlFilteringEngine.reset = function() {
filterDB.clear();
pselectors.clear();
duplicates.clear();
acceptedCount = 0;
discardedCount = 0;
};
htmlFilteringEngine.freeze = function() {
duplicates.clear();
filterDB.collectGarbage();
};
htmlFilteringEngine.compile = function(parser, writer) {
const isException = parser.isException();
const { raw, compiled } = parser.result;
if ( compiled === undefined ) {
return logError(writer, `Invalid HTML filter in {who}: ##${raw}`);
}
writer.select('HTML_FILTERS');
// Only exception filters are allowed to be global.
if ( parser.hasOptions() === false ) {
if ( isException ) {
writer.push([ 64, '', 1, compiled ]);
}
return;
}
const compiledFilters = [];
let hasOnlyNegated = true;
for ( const { hn, not, bad } of parser.getExtFilterDomainIterator() ) {
if ( bad ) { continue; }
let kind = isException ? 0b01 : 0b00;
if ( not ) {
kind ^= 0b01;
} else {
hasOnlyNegated = false;
}
if ( compiled.charCodeAt(0) === 0x7B /* '{' */ ) {
kind |= 0b10;
}
compiledFilters.push([ 64, hn, kind, compiled ]);
}
// Not allowed since it's equivalent to forbidden generic HTML filters
if ( isException === false && hasOnlyNegated ) {
return logError(writer, `Invalid HTML filter in {who}: ##${raw}`);
}
writer.pushMany(compiledFilters);
};
htmlFilteringEngine.fromCompiledContent = function(reader) {
// Don't bother loading filters if stream filtering is not supported.
if ( µb.canFilterResponseData === false ) { return; }
reader.select('HTML_FILTERS');
while ( reader.next() ) {
acceptedCount += 1;
const fingerprint = reader.fingerprint();
if ( duplicates.has(fingerprint) ) {
discardedCount += 1;
continue;
}
duplicates.add(fingerprint);
const args = reader.args();
filterDB.store(args[1], args[2], args[3]);
}
};
htmlFilteringEngine.retrieve = function(fctxt) {
const plains = new Set();
const procedurals = new Set();
const exceptions = new Set();
const retrieveSets = [ plains, exceptions, procedurals, exceptions ];
const hostname = fctxt.getHostname();
filterDB.retrieve(hostname, retrieveSets);
const domain = fctxt.getDomain();
const entity = entityFromDomain(domain);
const hostnameEntity = entity !== ''
? `${hostname.slice(0, -domain.length)}${entity}`
: '*';
filterDB.retrieve(hostnameEntity, retrieveSets, 1);
if ( plains.size === 0 && procedurals.size === 0 ) { return; }
// https://github.com/gorhill/uBlock/issues/2835
// Do not filter if the site is under an `allow` rule.
if (
µb.userSettings.advancedUserEnabled &&
sessionFirewall.evaluateCellZY(hostname, hostname, '*') === 2
) {
return;
}
const out = { plains, procedurals };
if ( exceptions.size === 0 ) {
return out;
}
for ( const selector of exceptions ) {
if ( plains.has(selector) ) {
plains.delete(selector);
logOne(fctxt, 1, selector);
continue;
}
if ( procedurals.has(selector) ) {
procedurals.delete(selector);
logOne(fctxt, 1, JSON.parse(selector).raw);
continue;
}
}
if ( plains.size !== 0 || procedurals.size !== 0 ) {
return out;
}
};
htmlFilteringEngine.apply = function(doc, details, selectors) {
docRegister = doc;
let modified = false;
for ( const selector of selectors.plains ) {
if ( applyCSSSelector(details, selector) ) {
modified = true;
}
}
for ( const selector of selectors.procedurals ) {
if ( applyProceduralSelector(details, selector) ) {
modified = true;
}
}
docRegister = undefined;
return modified;
};
htmlFilteringEngine.toSelfie = function() {
return filterDB.toSelfie();
};
htmlFilteringEngine.fromSelfie = function(selfie) {
filterDB.fromSelfie(selfie);
pselectors.clear();
};
/******************************************************************************/
export default htmlFilteringEngine;
/******************************************************************************/

View File

@@ -0,0 +1,213 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2021-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';
/******************************************************************************/
import logger from './logger.js';
import µb from './background.js';
import { entityFromDomain } from './uri-utils.js';
import { sessionFirewall } from './filtering-engines.js';
import { StaticExtFilteringHostnameDB } from './static-ext-filtering-db.js';
import * as sfp from './static-filtering-parser.js';
/******************************************************************************/
const duplicates = new Set();
const filterDB = new StaticExtFilteringHostnameDB(1);
const $headers = new Set();
const $exceptions = new Set();
let acceptedCount = 0;
let discardedCount = 0;
const headerIndexFromName = function(name, headers, start = 0) {
for ( let i = start; i < headers.length; i++ ) {
if ( headers[i].name.toLowerCase() !== name ) { continue; }
return i;
}
return -1;
};
const logOne = function(isException, token, fctxt) {
fctxt.duplicate()
.setRealm('extended')
.setType('header')
.setFilter({
modifier: true,
result: isException ? 2 : 1,
source: 'extended',
raw: `${(isException ? '#@#' : '##')}^responseheader(${token})`
})
.toLogger();
};
const httpheaderFilteringEngine = {
get acceptedCount() {
return acceptedCount;
},
get discardedCount() {
return discardedCount;
}
};
httpheaderFilteringEngine.reset = function() {
filterDB.clear();
duplicates.clear();
acceptedCount = 0;
discardedCount = 0;
};
httpheaderFilteringEngine.freeze = function() {
duplicates.clear();
filterDB.collectGarbage();
};
httpheaderFilteringEngine.compile = function(parser, writer) {
writer.select('HTTPHEADER_FILTERS');
const isException = parser.isException();
const root = parser.getBranchFromType(sfp.NODE_TYPE_EXT_PATTERN_RESPONSEHEADER);
const headerName = parser.getNodeString(root);
// Tokenless is meaningful only for exception filters.
if ( headerName === '' && isException === false ) { return; }
// Only exception filters are allowed to be global.
if ( parser.hasOptions() === false ) {
if ( isException ) {
writer.push([ 64, '', 1, headerName ]);
}
return;
}
// https://github.com/gorhill/uBlock/issues/3375
// Ignore instances of exception filter with negated hostnames,
// because there is no way to create an exception to an exception.
for ( const { hn, not, bad } of parser.getExtFilterDomainIterator() ) {
if ( bad ) { continue; }
let kind = 0;
if ( isException ) {
if ( not ) { continue; }
kind |= 1;
} else if ( not ) {
kind |= 1;
}
writer.push([ 64, hn, kind, headerName ]);
}
};
// 01234567890123456789
// responseheader(name)
// ^ ^
// 15 -1
httpheaderFilteringEngine.fromCompiledContent = function(reader) {
reader.select('HTTPHEADER_FILTERS');
while ( reader.next() ) {
acceptedCount += 1;
const fingerprint = reader.fingerprint();
if ( duplicates.has(fingerprint) ) {
discardedCount += 1;
continue;
}
duplicates.add(fingerprint);
const args = reader.args();
if ( args.length < 4 ) { continue; }
filterDB.store(args[1], args[2], args[3]);
}
};
httpheaderFilteringEngine.apply = function(fctxt, headers) {
if ( filterDB.size === 0 ) { return; }
const hostname = fctxt.getHostname();
if ( hostname === '' ) { return; }
const domain = fctxt.getDomain();
let entity = entityFromDomain(domain);
if ( entity !== '' ) {
entity = `${hostname.slice(0, -domain.length)}${entity}`;
} else {
entity = '*';
}
$headers.clear();
$exceptions.clear();
filterDB.retrieve(hostname, [ $headers, $exceptions ]);
filterDB.retrieve(entity, [ $headers, $exceptions ], 1);
if ( $headers.size === 0 ) { return; }
// https://github.com/gorhill/uBlock/issues/2835
// Do not filter response headers if the site is under an `allow` rule.
if (
µb.userSettings.advancedUserEnabled &&
sessionFirewall.evaluateCellZY(hostname, hostname, '*') === 2
) {
return;
}
const hasGlobalException = $exceptions.has('');
let modified = false;
let i = 0;
for ( const name of $headers ) {
const isExcepted = hasGlobalException || $exceptions.has(name);
if ( isExcepted ) {
if ( logger.enabled ) {
logOne(true, hasGlobalException ? '' : name, fctxt);
}
continue;
}
i = 0;
for (;;) {
i = headerIndexFromName(name, headers, i);
if ( i === -1 ) { break; }
headers.splice(i, 1);
if ( logger.enabled ) {
logOne(false, name, fctxt);
}
modified = true;
}
}
return modified;
};
httpheaderFilteringEngine.toSelfie = function() {
return filterDB.toSelfie();
};
httpheaderFilteringEngine.fromSelfie = function(selfie) {
filterDB.fromSelfie(selfie);
};
/******************************************************************************/
export default httpheaderFilteringEngine;
/******************************************************************************/

340
uBlock0.chromium/js/i18n.js Normal file
View File

@@ -0,0 +1,340 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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
*/
/******************************************************************************/
const i18n =
self.browser instanceof Object &&
self.browser instanceof Element === false
? self.browser.i18n
: self.chrome.i18n;
const i18n$ = (...args) => i18n.getMessage(...args);
/******************************************************************************/
const isBackgroundProcess = document.title === 'uBlock Origin Background Page';
if ( isBackgroundProcess !== true ) {
// http://www.w3.org/International/questions/qa-scripts#directions
document.body.setAttribute(
'dir',
['ar', 'he', 'fa', 'ps', 'ur'].indexOf(i18n$('@@ui_locale')) !== -1
? 'rtl'
: 'ltr'
);
// https://github.com/gorhill/uBlock/issues/2084
// Anything else than <a>, <b>, <code>, <em>, <i>, and <span> will
// be rendered as plain text.
// For <a>, only href attribute must be present, and it MUST starts with
// `https://`, and includes no single- or double-quotes.
// No HTML entities are allowed, there is code to handle existing HTML
// entities already present in translation files until they are all gone.
const allowedTags = new Set([
'a',
'b',
'code',
'em',
'i',
'span',
'u',
]);
const expandHtmlEntities = (( ) => {
const entities = new Map([
// TODO: Remove quote entities once no longer present in translation
// files. Other entities must stay.
[ '&shy;', '\u00AD' ],
[ '&ldquo;', '“' ],
[ '&rdquo;', '”' ],
[ '&lsquo;', '' ],
[ '&rsquo;', '' ],
[ '&lt;', '<' ],
[ '&gt;', '>' ],
]);
const decodeEntities = match => {
return entities.get(match) || match;
};
return function(text) {
if ( text.indexOf('&') !== -1 ) {
text = text.replace(/&[a-z]+;/g, decodeEntities);
}
return text;
};
})();
const safeTextToTextNode = function(text) {
return document.createTextNode(expandHtmlEntities(text));
};
const sanitizeElement = function(node) {
if ( allowedTags.has(node.localName) === false ) { return null; }
node.removeAttribute('style');
let child = node.firstElementChild;
while ( child !== null ) {
const next = child.nextElementSibling;
if ( sanitizeElement(child) === null ) {
child.remove();
}
child = next;
}
return node;
};
const safeTextToDOM = function(text, parent) {
if ( text === '' ) { return; }
// Fast path (most common).
if ( text.indexOf('<') === -1 ) {
const toInsert = safeTextToTextNode(text);
let toReplace = parent.childCount !== 0
? parent.firstChild
: null;
while ( toReplace !== null ) {
if ( toReplace.nodeType === 3 && toReplace.nodeValue === '_' ) {
break;
}
toReplace = toReplace.nextSibling;
}
if ( toReplace !== null ) {
parent.replaceChild(toInsert, toReplace);
} else {
parent.appendChild(toInsert);
}
return;
}
// Slow path.
// `<p>` no longer allowed. Code below can be removed once all <p>'s are
// gone from translation files.
text = text.replace(/^<p>|<\/p>/g, '')
.replace(/<p>/g, '\n\n');
// Parse allowed HTML tags.
const domParser = new DOMParser();
const parsedDoc = domParser.parseFromString(text, 'text/html');
let node = parsedDoc.body.firstChild;
while ( node !== null ) {
const next = node.nextSibling;
switch ( node.nodeType ) {
case 1: // element
if ( sanitizeElement(node) === null ) { break; }
parent.appendChild(node);
break;
case 3: // text
parent.appendChild(node);
break;
default:
break;
}
node = next;
}
};
i18n.safeTemplateToDOM = function(id, dict, parent) {
if ( parent === undefined ) {
parent = document.createDocumentFragment();
}
let textin = i18n$(id);
if ( textin === '' ) {
return parent;
}
if ( textin.indexOf('{{') === -1 ) {
safeTextToDOM(textin, parent);
return parent;
}
const re = /\{\{\w+\}\}/g;
let textout = '';
for (;;) {
const match = re.exec(textin);
if ( match === null ) {
textout += textin;
break;
}
textout += textin.slice(0, match.index);
let prop = match[0].slice(2, -2);
if ( Object.prototype.hasOwnProperty.call(dict, prop) ) {
textout += dict[prop].replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
} else {
textout += prop;
}
textin = textin.slice(re.lastIndex);
}
safeTextToDOM(textout, parent);
return parent;
};
// Helper to deal with the i18n'ing of HTML files.
i18n.render = function(context) {
const docu = document;
const root = context || docu;
for ( const elem of root.querySelectorAll('[data-i18n]') ) {
let text = i18n$(elem.getAttribute('data-i18n'));
if ( !text ) { continue; }
if ( text.indexOf('{{') === -1 ) {
safeTextToDOM(text, elem);
continue;
}
// Handle selector-based placeholders: these placeholders tell where
// existing child DOM element are to be positioned relative to the
// localized text nodes.
const parts = text.split(/(\{\{[^}]+\}\})/);
const fragment = document.createDocumentFragment();
let textBefore = '';
for ( let part of parts ) {
if ( part === '' ) { continue; }
if ( part.startsWith('{{') && part.endsWith('}}') ) {
// TODO: remove detection of ':' once it no longer appears
// in translation files.
const pos = part.indexOf(':');
if ( pos !== -1 ) {
part = part.slice(0, pos) + part.slice(-2);
}
const selector = part.slice(2, -2);
let node;
// Ideally, the i18n strings explicitly refer to the
// class of the element to insert. However for now we
// will create a class from what is currently found in
// the placeholder and first try to lookup the resulting
// selector. This way we don't have to revisit all
// translations just for the sake of declaring the proper
// selector in the placeholder field.
if ( selector.charCodeAt(0) !== 0x2E /* '.' */ ) {
node = elem.querySelector(`.${selector}`);
}
if ( node instanceof Element === false ) {
node = elem.querySelector(selector);
}
if ( node instanceof Element ) {
safeTextToDOM(textBefore, fragment);
fragment.appendChild(node);
textBefore = '';
continue;
}
}
textBefore += part;
}
if ( textBefore !== '' ) {
safeTextToDOM(textBefore, fragment);
}
elem.appendChild(fragment);
}
for ( const elem of root.querySelectorAll('[data-i18n-title]') ) {
const text = i18n$(elem.getAttribute('data-i18n-title'));
if ( !text ) { continue; }
elem.setAttribute('title', expandHtmlEntities(text));
}
for ( const elem of root.querySelectorAll('[placeholder]') ) {
const text = i18n$(elem.getAttribute('placeholder'));
if ( text === '' ) { continue; }
elem.setAttribute('placeholder', text);
}
for ( const elem of root.querySelectorAll('[data-i18n-tip]') ) {
const text = i18n$(elem.getAttribute('data-i18n-tip'))
.replace(/<br>/g, '\n')
.replace(/\n{3,}/g, '\n\n');
elem.setAttribute('data-tip', text);
if ( elem.getAttribute('aria-label') === 'data-tip' ) {
elem.setAttribute('aria-label', text);
}
}
};
i18n.renderElapsedTimeToString = function(tstamp) {
let value = (Date.now() - tstamp) / 60000;
if ( value < 2 ) {
return i18n$('elapsedOneMinuteAgo');
}
if ( value < 60 ) {
return i18n$('elapsedManyMinutesAgo').replace('{{value}}', Math.floor(value).toLocaleString());
}
value /= 60;
if ( value < 2 ) {
return i18n$('elapsedOneHourAgo');
}
if ( value < 24 ) {
return i18n$('elapsedManyHoursAgo').replace('{{value}}', Math.floor(value).toLocaleString());
}
value /= 24;
if ( value < 2 ) {
return i18n$('elapsedOneDayAgo');
}
return i18n$('elapsedManyDaysAgo').replace('{{value}}', Math.floor(value).toLocaleString());
};
const unicodeFlagToImageSrc = new Map([
[ '🇦🇱', 'al' ], [ '🇦🇷', 'ar' ], [ '🇦🇹', 'at' ], [ '🇧🇦', 'ba' ],
[ '🇧🇪', '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('|'),
'gu'
);
i18n.patchUnicodeFlags = function(text) {
const fragment = document.createDocumentFragment();
let i = 0;
for (;;) {
const match = reUnicodeFlags.exec(text);
if ( match === null ) { break; }
if ( match.index > i ) {
fragment.append(text.slice(i, match.index));
}
const img = document.createElement('img');
const countryCode = unicodeFlagToImageSrc.get(match[0]);
img.src = `/img/flags-of-the-world/${countryCode}.png`;
img.title = countryCode;
img.classList.add('countryFlag');
fragment.append(img, '\u200A');
i = reUnicodeFlags.lastIndex;
}
if ( i < text.length ) {
fragment.append(text.slice(i));
}
return fragment;
};
i18n.render();
}
/******************************************************************************/
export { i18n, i18n$ };

View File

@@ -0,0 +1,52 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2015 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
*/
// https://github.com/gorhill/uBlock/issues/533#issuecomment-164292868
// If WebRTC is supported, there won't be an exception if we
// try to instantiate a peer connection object.
// https://github.com/gorhill/uBlock/issues/533#issuecomment-168097594
// Because Chromium leaks WebRTC connections after they have been closed
// and forgotten, we need to test for WebRTC support inside an iframe, this
// way the closed and forgottetn WebRTC connections are properly garbage
// collected.
(function() {
'use strict';
var pc = null;
try {
var PC = self.RTCPeerConnection || self.webkitRTCPeerConnection;
if ( PC ) {
pc = new PC(null);
}
} catch (ex) {
console.error(ex);
}
if ( pc !== null ) {
pc.close();
}
window.top.postMessage(
pc !== null ? 'webRTCSupported' : 'webRTCNotSupported',
window.location.origin
);
})();

View File

@@ -0,0 +1,703 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2015-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
*/
import { dom, qs$, qsa$ } from './dom.js';
/******************************************************************************/
(( ) => {
/******************************************************************************/
const logger = self.logger;
const showdomButton = qs$('#showdom');
const inspector = qs$('#domInspector');
const domTree = qs$('#domTree');
const filterToIdMap = new Map();
let inspectedTabId = 0;
let inspectedHostname = '';
let uidGenerator = 1;
/*******************************************************************************
*
* How it works:
*
* 1. The logger/inspector is enabled from the logger window
*
* 2. The inspector content script is injected in the root frame of the tab
* currently selected in the logger
*
* 3. The inspector content script asks the logger/inspector to establish
* a two-way communication channel
*
* 3. The inspector content script embed an inspector frame in the document
* being inspected and waits for the inspector frame to be fully loaded
*
* 4. The inspector content script sends a messaging port object to the
* embedded inspector frame for a two-way communication channel between
* the inspector frame and the inspector content script
*
* 5. The inspector content script sends dom information to the
* logger/inspector
*
* */
const contentInspectorChannel = (( ) => {
let bcChannel;
let toContentPort;
const start = ( ) => {
bcChannel = new globalThis.BroadcastChannel('contentInspectorChannel');
bcChannel.onmessage = ev => {
const msg = ev.data || {};
connect(msg.tabId, msg.frameId);
};
browser.webNavigation.onDOMContentLoaded.addListener(onContentLoaded);
};
const shutdown = ( ) => {
browser.webNavigation.onDOMContentLoaded.removeListener(onContentLoaded);
disconnect();
bcChannel.close();
bcChannel.onmessage = null;
bcChannel = undefined;
};
const connect = (tabId, frameId) => {
disconnect();
try {
toContentPort = browser.tabs.connect(tabId, { frameId });
toContentPort.onMessage.addListener(onContentMessage);
toContentPort.onDisconnect.addListener(onContentDisconnect);
} catch(_) {
}
};
const disconnect = ( ) => {
if ( toContentPort === undefined ) { return; }
toContentPort.onMessage.removeListener(onContentMessage);
toContentPort.onDisconnect.removeListener(onContentDisconnect);
toContentPort.disconnect();
toContentPort = undefined;
};
const send = msg => {
if ( toContentPort === undefined ) { return; }
toContentPort.postMessage(msg);
};
const onContentMessage = msg => {
if ( msg.what === 'domLayoutFull' ) {
inspectedHostname = msg.hostname;
renderDOMFull(msg);
} else if ( msg.what === 'domLayoutIncremental' ) {
renderDOMIncremental(msg);
}
};
const onContentDisconnect = ( ) => {
disconnect();
};
const onContentLoaded = details => {
if ( details.tabId !== inspectedTabId ) { return; }
if ( details.frameId !== 0 ) { return; }
disconnect();
injectInspector();
};
return { start, disconnect, send, shutdown };
})();
/******************************************************************************/
const nodeFromDomEntry = entry => {
const li = document.createElement('li');
dom.attr(li, 'id', entry.nid);
// expander/collapser
li.appendChild(document.createElement('span'));
// selector
let node = document.createElement('code');
node.textContent = entry.sel;
li.appendChild(node);
// descendant count
let value = entry.cnt || 0;
node = document.createElement('span');
node.textContent = value !== 0 ? value.toLocaleString() : '';
dom.attr(node, 'data-cnt', value);
li.appendChild(node);
// cosmetic filter
if ( entry.filter === undefined ) {
return li;
}
node = document.createElement('code');
dom.cl.add(node, 'filter');
value = filterToIdMap.get(entry.filter);
if ( value === undefined ) {
value = `${uidGenerator}`;
filterToIdMap.set(entry.filter, value);
uidGenerator += 1;
}
dom.attr(node, 'data-filter-id', value);
node.textContent = entry.filter;
li.appendChild(node);
dom.cl.add(li, 'isCosmeticHide');
return li;
};
/******************************************************************************/
const appendListItem = (ul, li) => {
ul.appendChild(li);
// Ancestor nodes of a node which is affected by a cosmetic filter will
// be marked as "containing cosmetic filters", for user convenience.
if ( dom.cl.has(li, 'isCosmeticHide') === false ) { return; }
for (;;) {
li = li.parentElement.parentElement;
if ( li === null ) { break; }
dom.cl.add(li, 'hasCosmeticHide');
}
};
/******************************************************************************/
const renderDOMFull = response => {
const domTreeParent = domTree.parentElement;
let ul = domTreeParent.removeChild(domTree);
logger.removeAllChildren(domTree);
filterToIdMap.clear();
let lvl = 0;
let li;
for ( const entry of response.layout ) {
if ( entry.lvl === lvl ) {
li = nodeFromDomEntry(entry);
appendListItem(ul, li);
continue;
}
if ( entry.lvl > lvl ) {
ul = document.createElement('ul');
li.appendChild(ul);
dom.cl.add(li, 'branch');
li = nodeFromDomEntry(entry);
appendListItem(ul, li);
lvl = entry.lvl;
continue;
}
// entry.lvl < lvl
while ( entry.lvl < lvl ) {
ul = li.parentNode;
li = ul.parentNode;
ul = li.parentNode;
lvl -= 1;
}
li = nodeFromDomEntry(entry);
appendListItem(ul, li);
}
while ( ul.parentNode !== null ) {
ul = ul.parentNode;
}
dom.cl.add(ul.firstElementChild, 'show');
domTreeParent.appendChild(domTree);
};
/******************************************************************************/
const patchIncremental = (from, delta) => {
let li = from.parentElement.parentElement;
const patchCosmeticHide = delta >= 0 &&
dom.cl.has(from, 'isCosmeticHide') &&
dom.cl.has(li, 'hasCosmeticHide') === false;
// Include descendants count when removing a node
if ( delta < 0 ) {
delta -= countFromNode(from);
}
for ( ; li.localName === 'li'; li = li.parentElement.parentElement ) {
const span = li.children[2];
if ( delta !== 0 ) {
const cnt = countFromNode(li) + delta;
span.textContent = cnt !== 0 ? cnt.toLocaleString() : '';
dom.attr(span, 'data-cnt', cnt);
}
if ( patchCosmeticHide ) {
dom.cl.add(li, 'hasCosmeticHide');
}
}
};
/******************************************************************************/
const renderDOMIncremental = response => {
// Process each journal entry:
// 1 = node added
// -1 = node removed
const nodes = new Map(response.nodes);
let li = null;
let ul = null;
for ( const entry of response.journal ) {
// Remove node
if ( entry.what === -1 ) {
li = qs$(`#${entry.nid}`);
if ( li === null ) { continue; }
patchIncremental(li, -1);
li.parentNode.removeChild(li);
continue;
}
// Modify node
if ( entry.what === 0 ) {
// TODO: update selector/filter
continue;
}
// Add node as sibling
if ( entry.what === 1 && entry.l ) {
const previous = qs$(`#${entry.l}`);
// This should not happen
if ( previous === null ) {
// throw new Error('No left sibling!?');
continue;
}
ul = previous.parentElement;
li = nodeFromDomEntry(nodes.get(entry.nid));
ul.insertBefore(li, previous.nextElementSibling);
patchIncremental(li, 1);
continue;
}
// Add node as child
if ( entry.what === 1 && entry.u ) {
li = qs$(`#${entry.u}`);
// This should not happen
if ( li === null ) {
// throw new Error('No parent!?');
continue;
}
ul = qs$(li, 'ul');
if ( ul === null ) {
ul = document.createElement('ul');
li.appendChild(ul);
dom.cl.add(li, 'branch');
}
li = nodeFromDomEntry(nodes.get(entry.nid));
ul.appendChild(li);
patchIncremental(li, 1);
continue;
}
}
};
/******************************************************************************/
const countFromNode = li => {
const span = li.children[2];
const cnt = parseInt(dom.attr(span, 'data-cnt'), 10);
return isNaN(cnt) ? 0 : cnt;
};
/******************************************************************************/
const selectorFromNode = node => {
let selector = '';
while ( node !== null ) {
if ( node.localName === 'li' ) {
const code = qs$(node, 'code');
if ( code !== null ) {
selector = `${code.textContent} > ${selector}`;
if ( selector.includes('#') ) { break; }
}
}
node = node.parentElement;
}
return selector.slice(0, -3);
};
/******************************************************************************/
const selectorFromFilter = node => {
while ( node !== null ) {
if ( node.localName === 'li' ) {
const code = qs$(node, 'code:nth-of-type(2)');
if ( code !== null ) {
return code.textContent;
}
}
node = node.parentElement;
}
return '';
};
/******************************************************************************/
const nidFromNode = node => {
let li = node;
while ( li !== null ) {
if ( li.localName === 'li' ) {
return li.id || '';
}
li = li.parentElement;
}
return '';
};
/******************************************************************************/
const startDialog = (( ) => {
let dialog;
let textarea;
let hideSelectors = [];
let unhideSelectors = [];
const parse = function() {
hideSelectors = [];
unhideSelectors = [];
const re = /^([^#]*)(#@?#)(.+)$/;
for ( let line of textarea.value.split(/\s*\n\s*/) ) {
line = line.trim();
if ( line === '' || line.charAt(0) === '!' ) { continue; }
const matches = re.exec(line);
if ( matches === null || matches.length !== 4 ) { continue; }
if ( inspectedHostname.lastIndexOf(matches[1]) === -1 ) {
continue;
}
if ( matches[2] === '##' ) {
hideSelectors.push(matches[3]);
} else {
unhideSelectors.push(matches[3]);
}
}
showCommitted();
};
const inputTimer = vAPI.defer.create(parse);
const onInputChanged = ( ) => {
inputTimer.on(743);
};
const onClicked = function(ev) {
const target = ev.target;
ev.stopPropagation();
if ( target.id === 'createCosmeticFilters' ) {
vAPI.messaging.send('loggerUI', {
what: 'createUserFilter',
filters: textarea.value,
});
// Force a reload for the new cosmetic filter(s) to take effect
vAPI.messaging.send('loggerUI', {
what: 'reloadTab',
tabId: inspectedTabId,
});
return stop();
}
};
const showCommitted = function() {
contentInspectorChannel.send({
what: 'showCommitted',
hide: hideSelectors.join(',\n'),
unhide: unhideSelectors.join(',\n')
});
};
const showInteractive = function() {
contentInspectorChannel.send({
what: 'showInteractive',
hide: hideSelectors.join(',\n'),
unhide: unhideSelectors.join(',\n')
});
};
const start = function() {
dialog = logger.modalDialog.create('#cosmeticFilteringDialog', stop);
textarea = qs$(dialog, 'textarea');
hideSelectors = [];
for ( const node of qsa$(domTree, 'code.off') ) {
if ( dom.cl.has(node, 'filter') ) { continue; }
hideSelectors.push(selectorFromNode(node));
}
const taValue = [];
for ( const selector of hideSelectors ) {
taValue.push(inspectedHostname + '##' + selector);
}
const ids = new Set();
for ( const node of qsa$(domTree, 'code.filter.off') ) {
const id = dom.attr(node, 'data-filter-id');
if ( ids.has(id) ) { continue; }
ids.add(id);
unhideSelectors.push(node.textContent);
taValue.push(inspectedHostname + '#@#' + node.textContent);
}
textarea.value = taValue.join('\n');
textarea.addEventListener('input', onInputChanged);
dialog.addEventListener('click', onClicked, true);
showCommitted();
logger.modalDialog.show();
};
const stop = function() {
inputTimer.off();
showInteractive();
textarea.removeEventListener('input', onInputChanged);
dialog.removeEventListener('click', onClicked, true);
dialog = undefined;
textarea = undefined;
hideSelectors = [];
unhideSelectors = [];
};
return start;
})();
/******************************************************************************/
const onClicked = ev => {
ev.stopPropagation();
if ( inspectedTabId === 0 ) { return; }
const target = ev.target;
const parent = target.parentElement;
// Expand/collapse branch
if (
target.localName === 'span' &&
parent instanceof HTMLLIElement &&
dom.cl.has(parent, 'branch') &&
target === parent.firstElementChild
) {
const state = dom.cl.toggle(parent, 'show');
if ( !state ) {
for ( const node of qsa$(parent, '.branch') ) {
dom.cl.remove(node, 'show');
}
}
return;
}
// Not a node or filter
if ( target.localName !== 'code' ) { return; }
// Toggle cosmetic filter
if ( dom.cl.has(target, 'filter') ) {
contentInspectorChannel.send({
what: 'toggleFilter',
original: false,
target: dom.cl.toggle(target, 'off'),
selector: selectorFromNode(target),
filter: selectorFromFilter(target),
nid: nidFromNode(target)
});
dom.cl.toggle(
qsa$(inspector, `[data-filter-id="${dom.attr(target, 'data-filter-id')}"]`),
'off',
dom.cl.has(target, 'off')
);
}
// Toggle node
else {
contentInspectorChannel.send({
what: 'toggleNodes',
original: true,
target: dom.cl.toggle(target, 'off') === false,
selector: selectorFromNode(target),
nid: nidFromNode(target)
});
}
const cantCreate = qs$(domTree, '.off') === null;
dom.cl.toggle(qs$(inspector, '.permatoolbar .revert'), 'disabled', cantCreate);
dom.cl.toggle(qs$(inspector, '.permatoolbar .commit'), 'disabled', cantCreate);
};
/******************************************************************************/
const onMouseOver = (( ) => {
let mouseoverTarget = null;
const mouseoverTimer = vAPI.defer.create(( ) => {
contentInspectorChannel.send({
what: 'highlightOne',
selector: selectorFromNode(mouseoverTarget),
nid: nidFromNode(mouseoverTarget),
scrollTo: true
});
});
return ev => {
if ( inspectedTabId === 0 ) { return; }
// Convenience: skip real-time highlighting if shift key is pressed.
if ( ev.shiftKey ) { return; }
// Find closest `li`
const target = ev.target.closest('li');
if ( target === mouseoverTarget ) { return; }
mouseoverTarget = target;
mouseoverTimer.on(50);
};
})();
/******************************************************************************/
const currentTabId = ( ) => {
if ( dom.cl.has(showdomButton, 'active') === false ) { return 0; }
return logger.tabIdFromPageSelector();
};
/******************************************************************************/
const injectInspector = (( ) => {
const timer = vAPI.defer.create(( ) => {
const tabId = currentTabId();
if ( tabId <= 0 ) { return; }
inspectedTabId = tabId;
vAPI.messaging.send('loggerUI', {
what: 'scriptlet',
tabId,
scriptlet: 'dom-inspector',
});
});
return ( ) => {
shutdownInspector();
timer.offon(353);
};
})();
/******************************************************************************/
const shutdownInspector = ( ) => {
contentInspectorChannel.disconnect();
logger.removeAllChildren(domTree);
dom.cl.remove(inspector, 'vExpanded');
inspectedTabId = 0;
};
/******************************************************************************/
const onTabIdChanged = ( ) => {
const tabId = currentTabId();
if ( tabId <= 0 ) {
return toggleOff();
}
if ( inspectedTabId !== tabId ) {
injectInspector();
}
};
/******************************************************************************/
const toggleVExpandView = ( ) => {
const branches = qsa$('#domTree li.branch.show > ul > li.branch:not(.show)');
for ( const branch of branches ) {
dom.cl.add(branch, 'show');
}
};
const toggleVCompactView = ( ) => {
const branches = qsa$('#domTree li.branch.show > ul > li:not(.show)');
const tohideSet = new Set();
for ( const branch of branches ) {
const node = branch.closest('li.branch.show');
if ( node.id === 'n1' ) { continue; }
tohideSet.add(node);
}
const tohideList = Array.from(tohideSet);
let i = tohideList.length - 1;
while ( i > 0 ) {
if ( tohideList[i-1].contains(tohideList[i]) ) {
tohideList.splice(i-1, 1);
} else if ( tohideList[i].contains(tohideList[i-1]) ) {
tohideList.splice(i, 1);
}
i -= 1;
}
for ( const node of tohideList ) {
dom.cl.remove(node, 'show');
}
};
const toggleHCompactView = ( ) => {
dom.cl.toggle(inspector, 'hCompact');
};
/******************************************************************************/
const revert = ( ) => {
dom.cl.remove('#domTree .off', 'off');
contentInspectorChannel.send({ what: 'resetToggledNodes' });
dom.cl.add(qs$(inspector, '.permatoolbar .revert'), 'disabled');
dom.cl.add(qs$(inspector, '.permatoolbar .commit'), 'disabled');
};
/******************************************************************************/
const toggleOn = ( ) => {
dom.cl.add('#inspectors', 'dom');
window.addEventListener('beforeunload', toggleOff);
dom.on(document, 'tabIdChanged', onTabIdChanged);
dom.on(domTree, 'click', onClicked, true);
dom.on(domTree, 'mouseover', onMouseOver, true);
dom.on('#domInspector .vExpandToggler', 'click', toggleVExpandView);
dom.on('#domInspector .vCompactToggler', 'click', toggleVCompactView);
dom.on('#domInspector .hCompactToggler', 'click', toggleHCompactView);
dom.on('#domInspector .permatoolbar .revert', 'click', revert);
dom.on('#domInspector .permatoolbar .commit', 'click', startDialog);
contentInspectorChannel.start();
injectInspector();
};
/******************************************************************************/
const toggleOff = ( ) => {
dom.cl.remove(showdomButton, 'active');
dom.cl.remove('#inspectors', 'dom');
shutdownInspector();
window.removeEventListener('beforeunload', toggleOff);
dom.off(document, 'tabIdChanged', onTabIdChanged);
dom.off(domTree, 'click', onClicked, true);
dom.off(domTree, 'mouseover', onMouseOver, true);
dom.off('#domInspector .vExpandToggler', 'click', toggleVExpandView);
dom.off('#domInspector .vCompactToggler', 'click', toggleVCompactView);
dom.off('#domInspector .hCompactToggler', 'click', toggleHCompactView);
dom.off('#domInspector .permatoolbar .revert', 'click', revert);
dom.off('#domInspector .permatoolbar .commit', 'click', startDialog);
contentInspectorChannel.shutdown();
inspectedTabId = 0;
};
/******************************************************************************/
const toggle = ( ) => {
if ( dom.cl.toggle(showdomButton, 'active') ) {
toggleOn();
} else {
toggleOff();
}
};
dom.on(showdomButton, 'click', toggle);
/******************************************************************************/
})();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,92 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2015-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';
/******************************************************************************/
import { broadcast, broadcastToAll } from './broadcast.js';
/******************************************************************************/
let buffer = null;
let lastReadTime = 0;
let writePtr = 0;
// After 30 seconds without being read, the logger buffer will be considered
// unused, and thus disabled.
const logBufferObsoleteAfter = 30 * 1000;
const janitorTimer = vAPI.defer.create(( ) => {
if ( buffer === null ) { return; }
if ( lastReadTime >= (Date.now() - logBufferObsoleteAfter) ) {
return janitorTimer.on(logBufferObsoleteAfter);
}
logger.enabled = false;
buffer = null;
writePtr = 0;
logger.ownerId = undefined;
broadcastToAll({ what: 'loggerDisabled' });
});
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(details) {
if ( buffer === null ) { return; }
pushOne(boxEntry(details));
},
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;
},
};
/******************************************************************************/
export default logger;
/******************************************************************************/

190
uBlock0.chromium/js/lz4.js Normal file
View File

@@ -0,0 +1,190 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2018-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
*/
/* global lz4BlockCodec */
'use strict';
/******************************************************************************/
import µb from './background.js';
/*******************************************************************************
Experimental support for storage compression.
For background information on the topic, see:
https://github.com/uBlockOrigin/uBlock-issues/issues/141#issuecomment-407737186
**/
/******************************************************************************/
let promisedInstance;
let textEncoder, textDecoder;
let ttlCount = 0;
let ttlDelay = 60000;
const init = function() {
ttlDelay = µb.hiddenSettings.autoUpdateAssetFetchPeriod * 2 * 1000;
if ( promisedInstance === undefined ) {
let flavor;
if ( µb.hiddenSettings.disableWebAssembly === true ) {
flavor = 'js';
}
promisedInstance = lz4BlockCodec.createInstance(flavor);
}
return promisedInstance;
};
// We can't shrink memory usage of lz4 codec instances, and in the
// current case memory usage can grow to a significant amount given
// that a single contiguous memory buffer is required to accommodate
// both input and output data. Thus a time-to-live implementation
// which will cause the wasm instance to be forgotten after enough
// time elapse without the instance being used.
const destroy = function() {
//if ( lz4CodecInstance !== undefined ) {
// console.info(
// 'uBO: freeing lz4-block-codec instance (%s KB)',
// lz4CodecInstance.bytesInUse() >>> 10
// );
//}
promisedInstance = undefined;
textEncoder = textDecoder = undefined;
ttlCount = 0;
};
const ttlTimer = vAPI.defer.create(destroy);
const ttlManage = function(count) {
ttlTimer.off();
ttlCount += count;
if ( ttlCount > 0 ) { return; }
ttlTimer.on(ttlDelay);
};
const encodeValue = function(lz4CodecInstance, dataIn) {
if ( !lz4CodecInstance ) { return; }
//let t0 = window.performance.now();
if ( textEncoder === undefined ) {
textEncoder = new TextEncoder();
}
const inputArray = textEncoder.encode(dataIn);
const inputSize = inputArray.byteLength;
const outputArray = lz4CodecInstance.encodeBlock(inputArray, 8);
if ( outputArray instanceof Uint8Array === false ) { return; }
outputArray[0] = 0x18;
outputArray[1] = 0x4D;
outputArray[2] = 0x22;
outputArray[3] = 0x04;
outputArray[4] = (inputSize >>> 0) & 0xFF;
outputArray[5] = (inputSize >>> 8) & 0xFF;
outputArray[6] = (inputSize >>> 16) & 0xFF;
outputArray[7] = (inputSize >>> 24) & 0xFF;
//console.info(
// 'uBO: [%s] compressed %d KB => %d KB (%s%%) in %s ms',
// inputArray.byteLength >> 10,
// outputArray.byteLength >> 10,
// (outputArray.byteLength / inputArray.byteLength * 100).toFixed(0),
// (window.performance.now() - t0).toFixed(1)
//);
return outputArray;
};
const decodeValue = function(lz4CodecInstance, inputArray) {
if ( !lz4CodecInstance ) { return; }
//let t0 = window.performance.now();
if (
inputArray[0] !== 0x18 || inputArray[1] !== 0x4D ||
inputArray[2] !== 0x22 || inputArray[3] !== 0x04
) {
console.error('decodeValue: invalid input array');
return;
}
const outputSize =
(inputArray[4] << 0) | (inputArray[5] << 8) |
(inputArray[6] << 16) | (inputArray[7] << 24);
const outputArray = lz4CodecInstance.decodeBlock(inputArray, 8, outputSize);
if ( outputArray instanceof Uint8Array === false ) { return; }
if ( textDecoder === undefined ) {
textDecoder = new TextDecoder();
}
const s = textDecoder.decode(outputArray);
//console.info(
// 'uBO: [%s] decompressed %d KB => %d KB (%s%%) in %s ms',
// inputArray.byteLength >>> 10,
// outputSize >>> 10,
// (inputArray.byteLength / outputSize * 100).toFixed(0),
// (window.performance.now() - t0).toFixed(1)
//);
return s;
};
const lz4Codec = {
// Arguments:
// dataIn: must be a string
// Returns:
// A Uint8Array, or the input string as is if compression is not
// possible.
encode: async function(dataIn, serialize = undefined) {
if ( typeof dataIn !== 'string' || dataIn.length < 4096 ) {
return dataIn;
}
ttlManage(1);
const lz4CodecInstance = await init();
let dataOut = encodeValue(lz4CodecInstance, dataIn);
ttlManage(-1);
if ( serialize instanceof Function ) {
dataOut = await serialize(dataOut);
}
return dataOut || dataIn;
},
// Arguments:
// dataIn: must be a Uint8Array
// Returns:
// A string, or the input argument as is if decompression is not
// possible.
decode: async function(dataIn, deserialize = undefined) {
if ( deserialize instanceof Function ) {
dataIn = await deserialize(dataIn);
}
if ( dataIn instanceof Uint8Array === false ) {
return dataIn;
}
ttlManage(1);
const lz4CodecInstance = await init();
const dataOut = decodeValue(lz4CodecInstance, dataIn);
ttlManage(-1);
return dataOut || dataIn;
},
relinquish: function() {
ttlDelay = 1;
ttlManage(0);
},
};
/******************************************************************************/
export default lz4Codec;
/******************************************************************************/

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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';
export class MRUCache {
constructor(maxSize) {
this.maxSize = maxSize;
this.array = [];
this.map = new Map();
this.resetTime = Date.now();
}
add(key, value) {
const found = this.map.has(key);
this.map.set(key, value);
if ( found ) { return; }
if ( this.array.length === this.maxSize ) {
this.map.delete(this.array.pop());
}
this.array.unshift(key);
}
remove(key) {
if ( this.map.delete(key) === false ) { return; }
this.array.splice(this.array.indexOf(key), 1);
}
lookup(key) {
const value = this.map.get(key);
if ( value === undefined ) { return; }
if ( this.array[0] === key ) { return value; }
const i = this.array.indexOf(key);
this.array.copyWithin(1, 0, i);
this.array[0] = key;
return value;
}
reset() {
this.array = [];
this.map.clear();
this.resetTime = Date.now();
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,481 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2015-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';
/******************************************************************************/
import redirectableResources from './redirect-resources.js';
import { LineIterator, orphanizeString } from './text-utils.js';
/******************************************************************************/
const extToMimeMap = new Map([
[ 'css', 'text/css' ],
[ 'fn', 'fn/javascript' ], // invented mime type for internal use
[ 'gif', 'image/gif' ],
[ 'html', 'text/html' ],
[ 'js', 'text/javascript' ],
[ 'json', 'application/json' ],
[ 'mp3', 'audio/mp3' ],
[ 'mp4', 'video/mp4' ],
[ 'png', 'image/png' ],
[ 'txt', 'text/plain' ],
[ 'xml', 'text/xml' ],
]);
const typeToMimeMap = new Map([
[ 'main_frame', 'text/html' ],
[ 'other', 'text/plain' ],
[ 'script', 'text/javascript' ],
[ 'stylesheet', 'text/css' ],
[ 'sub_frame', 'text/html' ],
[ 'xmlhttprequest', 'text/plain' ],
]);
const validMimes = new Set(extToMimeMap.values());
const mimeFromName = name => {
const match = /\.([^.]+)$/.exec(name);
if ( match === null ) { return ''; }
return extToMimeMap.get(match[1]);
};
const removeTopCommentBlock = text => {
return text.replace(/^\/\*[\S\s]+?\n\*\/\s*/, '');
};
// vAPI.warSecret is optional, it could be absent in some environments,
// i.e. nodejs for example. Probably the best approach is to have the
// "web_accessible_resources secret" added outside by the client of this
// module, but for now I just want to remove an obstacle to modularization.
const warSecret = typeof vAPI === 'object' && vAPI !== null
? vAPI.warSecret.short
: ( ) => '';
const RESOURCES_SELFIE_VERSION = 7;
const RESOURCES_SELFIE_NAME = 'selfie/redirectEngine/resources';
/******************************************************************************/
/******************************************************************************/
class RedirectEntry {
constructor() {
this.mime = '';
this.data = '';
this.warURL = undefined;
this.params = undefined;
this.requiresTrust = false;
this.world = 'MAIN';
this.dependencies = [];
}
// Prevent redirection to web accessible resources when the request is
// of type 'xmlhttprequest', because XMLHttpRequest.responseURL would
// cause leakage of extension id. See:
// - https://stackoverflow.com/a/8056313
// - https://bugzilla.mozilla.org/show_bug.cgi?id=998076
// https://www.reddit.com/r/uBlockOrigin/comments/cpxm1v/
// User-supplied resources may already be base64 encoded.
toURL(fctxt, asDataURI = false) {
if (
this.warURL !== undefined &&
asDataURI !== true &&
fctxt instanceof Object &&
fctxt.type !== 'xmlhttprequest'
) {
const params = [];
const secret = warSecret();
if ( secret !== '' ) { params.push(`secret=${secret}`); }
if ( this.params !== undefined ) {
for ( const name of this.params ) {
const value = fctxt[name];
if ( value === undefined ) { continue; }
params.push(`${name}=${encodeURIComponent(value)}`);
}
}
let url = `${this.warURL}`;
if ( params.length !== 0 ) {
url += `?${params.join('&')}`;
}
return url;
}
if ( this.data === undefined ) { return; }
// https://github.com/uBlockOrigin/uBlock-issues/issues/701
if ( this.data === '' ) {
const mime = typeToMimeMap.get(fctxt.type);
if ( mime === '' ) { return; }
return `data:${mime},`;
}
if ( this.data.startsWith('data:') === false ) {
if ( this.mime.indexOf(';') === -1 ) {
this.data = `data:${this.mime};base64,${btoa(this.data)}`;
} else {
this.data = `data:${this.mime},${this.data}`;
}
}
return this.data;
}
toContent() {
if ( this.data.startsWith('data:') ) {
const pos = this.data.indexOf(',');
const base64 = this.data.endsWith(';base64', pos);
this.data = this.data.slice(pos + 1);
if ( base64 ) {
this.data = atob(this.data);
}
}
return this.data;
}
static fromDetails(details) {
const r = new RedirectEntry();
Object.assign(r, details);
return r;
}
}
/******************************************************************************/
/******************************************************************************/
class RedirectEngine {
constructor() {
this.aliases = new Map();
this.resources = new Map();
this.reset();
this.modifyTime = Date.now();
}
reset() {
}
freeze() {
}
tokenToURL(
fctxt,
token,
asDataURI = false
) {
const entry = this.resources.get(this.aliases.get(token) || token);
if ( entry === undefined ) { return; }
return entry.toURL(fctxt, asDataURI);
}
tokenToDNR(token) {
const entry = this.resources.get(this.aliases.get(token) || token);
if ( entry === undefined ) { return; }
if ( entry.warURL === undefined ) { return; }
return entry.warURL;
}
hasToken(token) {
if ( token === 'none' ) { return true; }
const asDataURI = token.charCodeAt(0) === 0x25 /* '%' */;
if ( asDataURI ) {
token = token.slice(1);
}
return this.resources.get(this.aliases.get(token) || token) !== undefined;
}
tokenRequiresTrust(token) {
const entry = this.resources.get(this.aliases.get(token) || token);
return entry && entry.requiresTrust === true || false;
}
async toSelfie() {
}
async fromSelfie() {
return true;
}
contentFromName(name, mime = '') {
const entry = this.resources.get(this.aliases.get(name) || name);
if ( entry === undefined ) { return; }
if ( entry.mime.startsWith(mime) === false ) { return; }
return {
js: entry.toContent(),
world: entry.world,
dependencies: entry.dependencies.slice(),
};
}
// https://github.com/uBlockOrigin/uAssets/commit/deefe8755511
// Consider 'none' a reserved keyword, to be used to disable redirection.
// https://github.com/uBlockOrigin/uBlock-issues/issues/1419
// Append newlines to raw text to ensure processing of trailing resource.
resourcesFromString(text) {
const lineIter = new LineIterator(
removeTopCommentBlock(text) + '\n\n'
);
const reNonEmptyLine = /\S/;
let fields, encoded, details;
while ( lineIter.eot() === false ) {
const line = lineIter.next();
if ( line.startsWith('#') ) { continue; }
if ( line.startsWith('// ') ) { continue; }
if ( fields === undefined ) {
if ( line === '' ) { continue; }
// Modern parser
if ( line.startsWith('/// ') ) {
const name = line.slice(4).trim();
fields = [ name, mimeFromName(name) ];
continue;
}
// Legacy parser
const head = line.trim().split(/\s+/);
if ( head.length !== 2 ) { continue; }
if ( head[0] === 'none' ) { continue; }
let pos = head[1].indexOf(';');
if ( pos === -1 ) { pos = head[1].length; }
if ( validMimes.has(head[1].slice(0, pos)) === false ) {
continue;
}
encoded = head[1].indexOf(';') !== -1;
fields = head;
continue;
}
if ( line.startsWith('/// ') ) {
if ( details === undefined ) {
details = [];
}
const [ prop, value ] = line.slice(4).trim().split(/\s+/);
if ( value !== undefined ) {
details.push({ prop, value });
}
continue;
}
if ( reNonEmptyLine.test(line) ) {
fields.push(encoded ? line.trim() : line);
continue;
}
// No more data, add the resource.
const name = this.aliases.get(fields[0]) || fields[0];
const mime = fields[1];
const data = orphanizeString(
fields.slice(2).join(encoded ? '' : '\n')
);
this.resources.set(name, RedirectEntry.fromDetails({ mime, data }));
if ( Array.isArray(details) ) {
const resource = this.resources.get(name);
for ( const { prop, value } of details ) {
switch ( prop ) {
case 'alias':
this.aliases.set(value, name);
break;
case 'world':
if ( /^isolated$/i.test(value) === false ) { break; }
resource.world = 'ISOLATED';
break;
case 'dependency':
if ( this.resources.has(value) === false ) { break; }
resource.dependencies.push(value);
break;
default:
break;
}
}
}
fields = undefined;
details = undefined;
}
this.modifyTime = Date.now();
}
loadBuiltinResources(fetcher) {
this.resources = new Map();
this.aliases = new Map();
const fetches = [
import('/js/resources/scriptlets.js').then(module => {
for ( const scriptlet of module.builtinScriptlets ) {
const details = {};
details.mime = mimeFromName(scriptlet.name);
details.data = scriptlet.fn.toString();
for ( const [ k, v ] of Object.entries(scriptlet) ) {
if ( k === 'fn' ) { continue; }
details[k] = v;
}
const entry = RedirectEntry.fromDetails(details);
this.resources.set(details.name, entry);
if ( Array.isArray(details.aliases) === false ) { continue; }
for ( const alias of details.aliases ) {
this.aliases.set(alias, details.name);
}
}
this.modifyTime = Date.now();
}).catch(reason => {
console.error(reason);
}),
];
const store = (name, data = undefined) => {
const details = redirectableResources.get(name);
const entry = RedirectEntry.fromDetails({
mime: mimeFromName(name),
data,
warURL: `/web_accessible_resources/${name}`,
params: details.params,
});
this.resources.set(name, entry);
if ( details.alias === undefined ) { return; }
if ( Array.isArray(details.alias) ) {
for ( const alias of details.alias ) {
this.aliases.set(alias, name);
}
} else {
this.aliases.set(details.alias, name);
}
};
const processBlob = (name, blob) => {
return new Promise(resolve => {
const reader = new FileReader();
reader.onload = ( ) => {
store(name, reader.result);
resolve();
};
reader.onabort = reader.onerror = ( ) => {
resolve();
};
reader.readAsDataURL(blob);
});
};
const processText = (name, text) => {
store(name, removeTopCommentBlock(text));
};
const process = result => {
const match = /^\/web_accessible_resources\/([^?]+)/.exec(result.url);
if ( match === null ) { return; }
const name = match[1];
return result.content instanceof Blob
? processBlob(name, result.content)
: processText(name, result.content);
};
for ( const [ name, details ] of redirectableResources ) {
if ( typeof details.data !== 'string' ) {
store(name);
continue;
}
fetches.push(
fetcher(`/web_accessible_resources/${name}`, {
responseType: details.data
}).then(
result => process(result)
)
);
}
return Promise.all(fetches);
}
getResourceDetails() {
const out = new Map([
[ 'none', { canInject: false, canRedirect: true, aliasOf: '' } ],
]);
for ( const [ name, entry ] of this.resources ) {
out.set(name, {
canInject: typeof entry.data === 'string',
canRedirect: entry.warURL !== undefined,
aliasOf: '',
extensionPath: entry.warURL,
});
}
for ( const [ alias, name ] of this.aliases ) {
const original = out.get(name);
if ( original === undefined ) { continue; }
const aliased = Object.assign({}, original);
aliased.aliasOf = name;
out.set(alias, aliased);
}
return Array.from(out).sort((a, b) => {
return a[0].localeCompare(b[0]);
});
}
getTrustedScriptletTokens() {
const out = [];
const isTrustedScriptlet = entry => {
if ( entry.requiresTrust !== true ) { return false; }
if ( entry.warURL !== undefined ) { return false; }
if ( typeof entry.data !== 'string' ) { return false; }
if ( entry.name.endsWith('.js') === false ) { return false; }
return true;
};
for ( const [ name, entry ] of this.resources ) {
if ( isTrustedScriptlet(entry) === false ) { continue; }
out.push(name.slice(0, -3));
}
for ( const [ alias, name ] of this.aliases ) {
if ( out.includes(name.slice(0, -3)) === false ) { continue; }
out.push(alias.slice(0, -3));
}
return out;
}
selfieFromResources(storage) {
return storage.toCache(RESOURCES_SELFIE_NAME, {
version: RESOURCES_SELFIE_VERSION,
aliases: this.aliases,
resources: this.resources,
});
}
async resourcesFromSelfie(storage) {
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;
}
invalidateResourcesSelfie(storage) {
storage.remove(RESOURCES_SELFIE_NAME);
}
}
/******************************************************************************/
const redirectEngine = new RedirectEngine();
export { redirectEngine };
/******************************************************************************/

View File

@@ -0,0 +1,192 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2015-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
*/
/******************************************************************************/
// The resources referenced below are found in ./web_accessible_resources/
//
// The content of the resources which declare a `data` property will be loaded
// in memory, and converted to a suitable internal format depending on the
// type of the loaded data. The `data` property allows for manual injection
// through `+js(...)`, or for redirection to a data: URI when a redirection
// to a web accessible resource is not desirable.
export default new Map([
[ '1x1.gif', {
alias: '1x1-transparent.gif',
data: 'blob',
} ],
[ '2x2.png', {
alias: '2x2-transparent.png',
data: 'blob',
} ],
[ '3x2.png', {
alias: '3x2-transparent.png',
data: 'blob',
} ],
[ '32x32.png', {
alias: '32x32-transparent.png',
data: 'blob',
} ],
[ 'amazon_ads.js', {
alias: 'amazon-adsystem.com/aax2/amzn_ads.js',
data: 'text',
} ],
[ 'amazon_apstag.js', {
} ],
[ 'ampproject_v0.js', {
alias: 'ampproject.org/v0.js',
} ],
[ 'chartbeat.js', {
alias: 'static.chartbeat.com/chartbeat.js',
} ],
[ 'click2load.html', {
params: [ 'aliasURL', 'url' ],
} ],
[ 'doubleclick_instream_ad_status.js', {
alias: 'doubleclick.net/instream/ad_status.js',
data: 'text',
} ],
[ 'empty', {
data: 'text', // Important!
} ],
[ 'fingerprint2.js', {
data: 'text',
} ],
[ 'fingerprint3.js', {
data: 'text',
} ],
[ 'google-analytics_analytics.js', {
alias: [
'google-analytics.com/analytics.js',
'googletagmanager_gtm.js',
'googletagmanager.com/gtm.js'
],
data: 'text',
} ],
[ 'google-analytics_cx_api.js', {
alias: 'google-analytics.com/cx/api.js',
} ],
[ 'google-analytics_ga.js', {
alias: 'google-analytics.com/ga.js',
data: 'text',
} ],
[ 'google-analytics_inpage_linkid.js', {
alias: 'google-analytics.com/inpage_linkid.js',
} ],
[ 'google-ima.js', {
alias: 'google-ima3', /* adguard compatibility */
} ],
[ 'googlesyndication_adsbygoogle.js', {
alias: [
'googlesyndication.com/adsbygoogle.js',
'googlesyndication-adsbygoogle', /* adguard compatibility */
],
data: 'text',
} ],
[ 'googletagservices_gpt.js', {
alias: [
'googletagservices.com/gpt.js',
'googletagservices-gpt', /* adguard compatibility */
],
data: 'text',
} ],
[ 'hd-main.js', {
} ],
[ 'nobab.js', {
alias: [ 'bab-defuser.js', 'prevent-bab.js' ],
data: 'text',
} ],
[ 'nobab2.js', {
data: 'text',
} ],
[ 'noeval.js', {
data: 'text',
} ],
[ 'noeval-silent.js', {
alias: 'silent-noeval.js',
data: 'text',
} ],
[ 'nofab.js', {
alias: 'fuckadblock.js-3.2.0',
data: 'text',
} ],
[ 'noop-0.1s.mp3', {
alias: [ 'noopmp3-0.1s', 'abp-resource:blank-mp3' ],
data: 'blob',
} ],
[ 'noop-0.5s.mp3', {
} ],
[ 'noop-1s.mp4', {
alias: [ 'noopmp4-1s', 'abp-resource:blank-mp4' ],
data: 'blob',
} ],
[ 'noop.css', {
data: 'text',
} ],
[ 'noop.html', {
alias: 'noopframe',
} ],
[ 'noop.js', {
alias: [ 'noopjs', 'abp-resource:blank-js' ],
data: 'text',
} ],
[ 'noop.json', {
alias: [ 'noopjson' ],
data: 'text',
} ],
[ 'noop.txt', {
alias: 'nooptext',
data: 'text',
} ],
[ 'noop-vast2.xml', {
alias: 'noopvast-2.0',
data: 'text',
} ],
[ 'noop-vast3.xml', {
alias: 'noopvast-3.0',
data: 'text',
} ],
[ 'noop-vast4.xml', {
alias: 'noopvast-4.0',
data: 'text',
} ],
[ 'noop-vmap1.xml', {
alias: [ 'noop-vmap1.0.xml', 'noopvmap-1.0' ],
data: 'text',
} ],
[ 'outbrain-widget.js', {
alias: 'widgets.outbrain.com/outbrain.js',
} ],
[ 'popads.js', {
alias: [ 'popads.net.js', 'prevent-popads-net.js' ],
data: 'text',
} ],
[ 'popads-dummy.js', {
data: 'text',
} ],
[ 'prebid-ads.js', {
data: 'text',
} ],
[ 'scorecardresearch_beacon.js', {
alias: 'scorecardresearch.com/beacon.js',
} ],
]);

View File

@@ -0,0 +1,305 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2019-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
*/
import { registerScriptlet } from './base.js';
import { runAt } from './run-at.js';
import { safeSelf } from './safe-self.js';
/******************************************************************************/
export function setAttrFn(
trusted = false,
logPrefix,
selector = '',
attr = '',
value = ''
) {
if ( selector === '' ) { return; }
if ( attr === '' ) { return; }
const safe = safeSelf();
const copyFrom = trusted === false && /^\[.+\]$/.test(value)
? value.slice(1, -1)
: '';
const extractValue = elem => copyFrom !== ''
? elem.getAttribute(copyFrom) || ''
: value;
const applySetAttr = ( ) => {
let elems;
try {
elems = document.querySelectorAll(selector);
} catch(_) {
return false;
}
for ( const elem of elems ) {
const before = elem.getAttribute(attr);
const after = extractValue(elem);
if ( after === before ) { continue; }
if ( after !== '' && /^on/i.test(attr) ) {
if ( attr.toLowerCase() in elem ) { continue; }
}
elem.setAttribute(attr, after);
safe.uboLog(logPrefix, `${attr}="${after}"`);
}
return true;
};
let observer, timer;
const onDomChanged = mutations => {
if ( timer !== undefined ) { return; }
let shouldWork = false;
for ( const mutation of mutations ) {
if ( mutation.addedNodes.length === 0 ) { continue; }
for ( const node of mutation.addedNodes ) {
if ( node.nodeType !== 1 ) { continue; }
shouldWork = true;
break;
}
if ( shouldWork ) { break; }
}
if ( shouldWork === false ) { return; }
timer = self.requestAnimationFrame(( ) => {
timer = undefined;
applySetAttr();
});
};
const start = ( ) => {
if ( applySetAttr() === false ) { return; }
observer = new MutationObserver(onDomChanged);
observer.observe(document.body, {
subtree: true,
childList: true,
});
};
runAt(( ) => { start(); }, 'idle');
}
registerScriptlet(setAttrFn, {
name: 'set-attr.fn',
dependencies: [
runAt,
safeSelf,
],
});
/**
* @scriptlet set-attr
*
* @description
* Sets the specified attribute on the specified elements. This scriptlet runs
* once when the page loads then afterward on DOM mutations.
*
* Reference: https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-attr.js
*
* @param selector
* A CSS selector for the elements to target.
*
* @param attr
* The name of the attribute to modify.
*
* @param value
* The new value of the attribute. Supported values:
* - `''`: empty string (default)
* - `true`
* - `false`
* - positive decimal integer 0 <= value < 32768
* - `[other]`: copy the value from attribute `other` on the same element
*
* */
export function setAttr(
selector = '',
attr = '',
value = ''
) {
const safe = safeSelf();
const logPrefix = safe.makeLogPrefix('set-attr', selector, attr, value);
const validValues = [ '', 'false', 'true' ];
if ( validValues.includes(value.toLowerCase()) === false ) {
if ( /^\d+$/.test(value) ) {
const n = parseInt(value, 10);
if ( n >= 32768 ) { return; }
value = `${n}`;
} else if ( /^\[.+\]$/.test(value) === false ) {
return;
}
}
setAttrFn(false, logPrefix, selector, attr, value);
}
registerScriptlet(setAttr, {
name: 'set-attr.js',
dependencies: [
safeSelf,
setAttrFn,
],
world: 'ISOLATED',
});
/**
* @trustedScriptlet trusted-set-attr
*
* @description
* Sets the specified attribute on the specified elements. This scriptlet runs
* once when the page loads then afterward on DOM mutations.
*
* Reference: https://github.com/AdguardTeam/Scriptlets/blob/master/wiki/about-trusted-scriptlets.md#-%EF%B8%8F-trusted-set-attr
*
* @param selector
* A CSS selector for the elements to target.
*
* @param attr
* The name of the attribute to modify.
*
* @param value
* The new value of the attribute. Since the scriptlet requires a trusted
* source, the value can be anything.
*
* */
export function trustedSetAttr(
selector = '',
attr = '',
value = ''
) {
const safe = safeSelf();
const logPrefix = safe.makeLogPrefix('trusted-set-attr', selector, attr, value);
setAttrFn(true, logPrefix, selector, attr, value);
}
registerScriptlet(trustedSetAttr, {
name: 'trusted-set-attr.js',
requiresTrust: true,
dependencies: [
safeSelf,
setAttrFn,
],
world: 'ISOLATED',
});
/**
* @scriptlet remove-attr
*
* @description
* Remove one or more attributes from a set of elements.
*
* @param attribute
* The name of the attribute(s) to remove. This can be a list of space-
* separated attribute names.
*
* @param [selector]
* Optional. A CSS selector for the elements to target. Default to
* `[attribute]`, or `[attribute1],[attribute2],...` if more than one
* attribute name is specified.
*
* @param [behavior]
* Optional. Space-separated tokens which modify the default behavior.
* - `asap`: Try to remove the attribute as soon as possible. Default behavior
* is to remove the attribute(s) asynchronously.
* - `stay`: Keep trying to remove the specified attribute(s) on DOM mutations.
* */
export function removeAttr(
rawToken = '',
rawSelector = '',
behavior = ''
) {
if ( typeof rawToken !== 'string' ) { return; }
if ( rawToken === '' ) { return; }
const safe = safeSelf();
const logPrefix = safe.makeLogPrefix('remove-attr', rawToken, rawSelector, behavior);
const tokens = safe.String_split.call(rawToken, /\s*\|\s*/);
const selector = tokens
.map(a => `${rawSelector}[${CSS.escape(a)}]`)
.join(',');
if ( safe.logLevel > 1 ) {
safe.uboLog(logPrefix, `Target selector:\n\t${selector}`);
}
const asap = /\basap\b/.test(behavior);
let timerId;
const rmattrAsync = ( ) => {
if ( timerId !== undefined ) { return; }
timerId = safe.onIdle(( ) => {
timerId = undefined;
rmattr();
}, { timeout: 17 });
};
const rmattr = ( ) => {
if ( timerId !== undefined ) {
safe.offIdle(timerId);
timerId = undefined;
}
try {
const nodes = document.querySelectorAll(selector);
for ( const node of nodes ) {
for ( const attr of tokens ) {
if ( node.hasAttribute(attr) === false ) { continue; }
node.removeAttribute(attr);
safe.uboLog(logPrefix, `Removed attribute '${attr}'`);
}
}
} catch(ex) {
}
};
const mutationHandler = mutations => {
if ( timerId !== undefined ) { return; }
let skip = true;
for ( let i = 0; i < mutations.length && skip; i++ ) {
const { type, addedNodes, removedNodes } = mutations[i];
if ( type === 'attributes' ) { skip = false; }
for ( let j = 0; j < addedNodes.length && skip; j++ ) {
if ( addedNodes[j].nodeType === 1 ) { skip = false; break; }
}
for ( let j = 0; j < removedNodes.length && skip; j++ ) {
if ( removedNodes[j].nodeType === 1 ) { skip = false; break; }
}
}
if ( skip ) { return; }
asap ? rmattr() : rmattrAsync();
};
const start = ( ) => {
rmattr();
if ( /\bstay\b/.test(behavior) === false ) { return; }
const observer = new MutationObserver(mutationHandler);
observer.observe(document, {
attributes: true,
attributeFilter: tokens,
childList: true,
subtree: true,
});
};
runAt(( ) => { start(); }, safe.String_split.call(behavior, /\s+/));
}
registerScriptlet(removeAttr, {
name: 'remove-attr.js',
aliases: [
'ra.js',
],
dependencies: [
runAt,
safeSelf,
],
});
/******************************************************************************/

View File

@@ -0,0 +1,38 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2019-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
*/
export const registeredScriptlets = [];
export const registerScriptlet = (fn, details) => {
if ( typeof details !== 'object' ) {
throw new ReferenceError('Missing scriptlet details');
}
details.fn = fn;
fn.details = details;
if ( Array.isArray(details.dependencies) ) {
details.dependencies.forEach((fn, i, array) => {
if ( typeof fn !== 'function' ) { return; }
array[i] = fn.details.name;
});
}
registeredScriptlets.push(details);
};

View File

@@ -0,0 +1,419 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2019-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
*/
import { registerScriptlet } from './base.js';
import { safeSelf } from './safe-self.js';
/******************************************************************************/
export function getSafeCookieValuesFn() {
return [
'accept', 'reject',
'accepted', 'rejected', 'notaccepted',
'allow', 'disallow', 'deny',
'allowed', 'denied',
'approved', 'disapproved',
'checked', 'unchecked',
'dismiss', 'dismissed',
'enable', 'disable',
'enabled', 'disabled',
'essential', 'nonessential',
'forbidden', 'forever',
'hide', 'hidden',
'necessary', 'required',
'ok',
'on', 'off',
'true', 't', 'false', 'f',
'yes', 'y', 'no', 'n',
'all', 'none', 'functional',
'granted', 'done',
];
}
registerScriptlet(getSafeCookieValuesFn, {
name: 'get-safe-cookie-values.fn',
});
/******************************************************************************/
export function getAllCookiesFn() {
const safe = safeSelf();
return safe.String_split.call(document.cookie, /\s*;\s*/).map(s => {
const pos = s.indexOf('=');
if ( pos === 0 ) { return; }
if ( pos === -1 ) { return `${s.trim()}=`; }
const key = s.slice(0, pos).trim();
const value = s.slice(pos+1).trim();
return { key, value };
}).filter(s => s !== undefined);
}
registerScriptlet(getAllCookiesFn, {
name: 'get-all-cookies.fn',
dependencies: [
safeSelf,
],
});
/******************************************************************************/
export function getCookieFn(
name = ''
) {
const safe = safeSelf();
for ( const s of safe.String_split.call(document.cookie, /\s*;\s*/) ) {
const pos = s.indexOf('=');
if ( pos === -1 ) { continue; }
if ( s.slice(0, pos) !== name ) { continue; }
return s.slice(pos+1).trim();
}
}
registerScriptlet(getCookieFn, {
name: 'get-cookie.fn',
dependencies: [
safeSelf,
],
});
/******************************************************************************/
export function setCookieFn(
trusted = false,
name = '',
value = '',
expires = '',
path = '',
options = {},
) {
// https://datatracker.ietf.org/doc/html/rfc2616#section-2.2
// https://github.com/uBlockOrigin/uBlock-issues/issues/2777
if ( trusted === false && /[^!#$%&'*+\-.0-9A-Z[\]^_`a-z|~]/.test(name) ) {
name = encodeURIComponent(name);
}
// https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1
// The characters [",] are given a pass from the RFC requirements because
// apparently browsers do not follow the RFC to the letter.
if ( /[^ -:<-[\]-~]/.test(value) ) {
value = encodeURIComponent(value);
}
const cookieBefore = getCookieFn(name);
if ( cookieBefore !== undefined && options.dontOverwrite ) { return; }
if ( cookieBefore === value && options.reload ) { return; }
const cookieParts = [ name, '=', value ];
if ( expires !== '' ) {
cookieParts.push('; expires=', expires);
}
if ( path === '' ) { path = '/'; }
else if ( path === 'none' ) { path = ''; }
if ( path !== '' && path !== '/' ) { return; }
if ( path === '/' ) {
cookieParts.push('; path=/');
}
if ( trusted ) {
if ( options.domain ) {
cookieParts.push(`; domain=${options.domain}`);
}
cookieParts.push('; Secure');
} else if ( /^__(Host|Secure)-/.test(name) ) {
cookieParts.push('; Secure');
}
try {
document.cookie = cookieParts.join('');
} catch(_) {
}
const done = getCookieFn(name) === value;
if ( done && options.reload ) {
window.location.reload();
}
return done;
}
registerScriptlet(setCookieFn, {
name: 'set-cookie.fn',
dependencies: [
getCookieFn,
],
});
/**
* @scriptlet set-cookie
*
* @description
* Set a cookie to a safe value.
*
* @param name
* The name of the cookie to set.
*
* @param value
* The value of the cookie to set. Must be a safe value. Unsafe values will be
* ignored and no cookie will be set. See getSafeCookieValuesFn() helper above.
*
* @param [path]
* Optional. The path of the cookie to set. Default to `/`.
*
* Reference:
* https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-cookie.js
* */
export function setCookie(
name = '',
value = '',
path = ''
) {
if ( name === '' ) { return; }
const safe = safeSelf();
const logPrefix = safe.makeLogPrefix('set-cookie', name, value, path);
const normalized = value.toLowerCase();
const match = /^("?)(.+)\1$/.exec(normalized);
const unquoted = match && match[2] || normalized;
const validValues = getSafeCookieValuesFn();
if ( validValues.includes(unquoted) === false ) {
if ( /^-?\d+$/.test(unquoted) === false ) { return; }
const n = parseInt(value, 10) || 0;
if ( n < -32767 || n > 32767 ) { return; }
}
const done = setCookieFn(
false,
name,
value,
'',
path,
safe.getExtraArgs(Array.from(arguments), 3)
);
if ( done ) {
safe.uboLog(logPrefix, 'Done');
}
}
registerScriptlet(setCookie, {
name: 'set-cookie.js',
world: 'ISOLATED',
dependencies: [
getSafeCookieValuesFn,
safeSelf,
setCookieFn,
],
});
// For compatibility with AdGuard
export function setCookieReload(name, value, path, ...args) {
setCookie(name, value, path, 'reload', '1', ...args);
}
registerScriptlet(setCookieReload, {
name: 'set-cookie-reload.js',
world: 'ISOLATED',
dependencies: [
setCookie,
],
});
/**
* @trustedScriptlet trusted-set-cookie
*
* @description
* Set a cookie to any value. This scriptlet can be used only from a trusted
* source.
*
* @param name
* The name of the cookie to set.
*
* @param value
* The value of the cookie to set. Must be a safe value. Unsafe values will be
* ignored and no cookie will be set. See getSafeCookieValuesFn() helper above.
*
* @param [offsetExpiresSec]
* Optional. The path of the cookie to set. Default to `/`.
*
* @param [path]
* Optional. The path of the cookie to set. Default to `/`.
*
* Reference:
* https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-cookie.js
* */
export function trustedSetCookie(
name = '',
value = '',
offsetExpiresSec = '',
path = ''
) {
if ( name === '' ) { return; }
const safe = safeSelf();
const logPrefix = safe.makeLogPrefix('set-cookie', name, value, path);
const time = new Date();
if ( value.includes('$now$') ) {
value = value.replaceAll('$now$', time.getTime());
}
if ( value.includes('$currentDate$') ) {
value = value.replaceAll('$currentDate$', time.toUTCString());
}
if ( value.includes('$currentISODate$') ) {
value = value.replaceAll('$currentISODate$', time.toISOString());
}
let expires = '';
if ( offsetExpiresSec !== '' ) {
if ( offsetExpiresSec === '1day' ) {
time.setDate(time.getDate() + 1);
} else if ( offsetExpiresSec === '1year' ) {
time.setFullYear(time.getFullYear() + 1);
} else {
if ( /^\d+$/.test(offsetExpiresSec) === false ) { return; }
time.setSeconds(time.getSeconds() + parseInt(offsetExpiresSec, 10));
}
expires = time.toUTCString();
}
const done = setCookieFn(
true,
name,
value,
expires,
path,
safeSelf().getExtraArgs(Array.from(arguments), 4)
);
if ( done ) {
safe.uboLog(logPrefix, 'Done');
}
}
registerScriptlet(trustedSetCookie, {
name: 'trusted-set-cookie.js',
requiresTrust: true,
world: 'ISOLATED',
dependencies: [
safeSelf,
setCookieFn,
],
});
// For compatibility with AdGuard
export function trustedSetCookieReload(name, value, offsetExpiresSec, path, ...args) {
trustedSetCookie(name, value, offsetExpiresSec, path, 'reload', '1', ...args);
}
registerScriptlet(trustedSetCookieReload, {
name: 'trusted-set-cookie-reload.js',
requiresTrust: true,
world: 'ISOLATED',
dependencies: [
trustedSetCookie,
],
});
/**
* @scriptlet remove-cookie
*
* @description
* Removes current site cookies specified by name. The removal operation occurs
* immediately when the scriptlet is injected, then when the page is unloaded.
*
* @param needle
* A string or a regex matching the name of the cookie(s) to remove.
*
* @param ['when', token]
* Vararg, optional. The parameter following 'when' tells when extra removal
* operations should take place.
* - `scroll`: when the page is scrolled
* - `keydown`: when a keyboard touch is pressed
*
* */
export function removeCookie(
needle = ''
) {
if ( typeof needle !== 'string' ) { return; }
const safe = safeSelf();
const reName = safe.patternToRegex(needle);
const extraArgs = safe.getExtraArgs(Array.from(arguments), 1);
const throttle = (fn, ms = 500) => {
if ( throttle.timer !== undefined ) { return; }
throttle.timer = setTimeout(( ) => {
throttle.timer = undefined;
fn();
}, ms);
};
const remove = ( ) => {
safe.String_split.call(document.cookie, ';').forEach(cookieStr => {
const pos = cookieStr.indexOf('=');
if ( pos === -1 ) { return; }
const cookieName = cookieStr.slice(0, pos).trim();
if ( reName.test(cookieName) === false ) { return; }
const part1 = cookieName + '=';
const part2a = '; domain=' + document.location.hostname;
const part2b = '; domain=.' + document.location.hostname;
let part2c, part2d;
const domain = document.domain;
if ( domain ) {
if ( domain !== document.location.hostname ) {
part2c = '; domain=.' + domain;
}
if ( domain.startsWith('www.') ) {
part2d = '; domain=' + domain.replace('www', '');
}
}
const part3 = '; path=/';
const part4 = '; Max-Age=-1000; expires=Thu, 01 Jan 1970 00:00:00 GMT';
document.cookie = part1 + part4;
document.cookie = part1 + part2a + part4;
document.cookie = part1 + part2b + part4;
document.cookie = part1 + part3 + part4;
document.cookie = part1 + part2a + part3 + part4;
document.cookie = part1 + part2b + part3 + part4;
if ( part2c !== undefined ) {
document.cookie = part1 + part2c + part3 + part4;
}
if ( part2d !== undefined ) {
document.cookie = part1 + part2d + part3 + part4;
}
});
};
remove();
window.addEventListener('beforeunload', remove);
if ( typeof extraArgs.when !== 'string' ) { return; }
const supportedEventTypes = [ 'scroll', 'keydown' ];
const eventTypes = safe.String_split.call(extraArgs.when, /\s/);
for ( const type of eventTypes ) {
if ( supportedEventTypes.includes(type) === false ) { continue; }
document.addEventListener(type, ( ) => {
throttle(remove);
}, { passive: true });
}
}
registerScriptlet(removeCookie, {
name: 'remove-cookie.js',
aliases: [
'cookie-remover.js',
],
world: 'ISOLATED',
dependencies: [
safeSelf,
],
});
/******************************************************************************/

View File

@@ -0,0 +1,188 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2019-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
*/
import { registerScriptlet } from './base.js';
import { runAt } from './run-at.js';
import { safeSelf } from './safe-self.js';
import { urlSkip } from '../urlskip.js';
/******************************************************************************/
registerScriptlet(urlSkip, {
name: 'urlskip.fn',
});
/**
* @scriptlet href-sanitizer
*
* @description
* Set the `href` attribute to a value found in the DOM at, or below the
* targeted `a` element, and optionally with transformation steps.
*
* @param selector
* A plain CSS selector for elements which `href` property must be sanitized.
*
* @param source
* One or more tokens to lookup the source of the `href` property, and
* optionally the transformation steps to perform:
* - `text`: Use the text content of the element as the URL
* - `[name]`: Use the value of the attribute `name` as the URL
* - Transformation steps: see `urlskip` documentation
*
* If `text` or `[name]` is not present, the URL will be the value of `href`
* attribute.
*
* @example
* `example.org##+js(href-sanitizer, a)`
* `example.org##+js(href-sanitizer, a[title], [title])`
* `example.org##+js(href-sanitizer, a[href*="/away.php?to="], ?to)`
* `example.org##+js(href-sanitizer, a[href*="/redirect"], ?url ?url -base64)`
*
* */
function hrefSanitizer(
selector = '',
source = ''
) {
if ( typeof selector !== 'string' ) { return; }
if ( selector === '' ) { return; }
const safe = safeSelf();
const logPrefix = safe.makeLogPrefix('href-sanitizer', selector, source);
if ( source === '' ) { source = 'text'; }
const sanitizeCopycats = (href, text) => {
let elems = [];
try {
elems = document.querySelectorAll(`a[href="${href}"`);
}
catch(ex) {
}
for ( const elem of elems ) {
elem.setAttribute('href', text);
}
return elems.length;
};
const validateURL = text => {
if ( typeof text !== 'string' ) { return ''; }
if ( text === '' ) { return ''; }
if ( /[\x00-\x20\x7f]/.test(text) ) { return ''; }
try {
const url = new URL(text, document.location);
return url.href;
} catch(ex) {
}
return '';
};
const extractParam = (href, source) => {
if ( Boolean(source) === false ) { return href; }
const recursive = source.includes('?', 1);
const end = recursive ? source.indexOf('?', 1) : source.length;
try {
const url = new URL(href, document.location);
let value = url.searchParams.get(source.slice(1, end));
if ( value === null ) { return href }
if ( recursive ) { return extractParam(value, source.slice(end)); }
return value;
} catch(x) {
}
return href;
};
const extractURL = (elem, source) => {
if ( /^\[.*\]$/.test(source) ) {
return elem.getAttribute(source.slice(1,-1).trim()) || '';
}
if ( source === 'text' ) {
return elem.textContent
.replace(/^[^\x21-\x7e]+/, '') // remove leading invalid characters
.replace(/[^\x21-\x7e]+$/, '') // remove trailing invalid characters
;
}
if ( source.startsWith('?') === false ) { return ''; }
const steps = source.replace(/(\S)\?/g, '\\1?').split(/\s+/);
const url = steps.length === 1
? extractParam(elem.href, source)
: urlSkip(elem.href, false, steps);
if ( url === undefined ) { return; }
return url.replace(/ /g, '%20');
};
const sanitize = ( ) => {
let elems = [];
try {
elems = document.querySelectorAll(selector);
}
catch(ex) {
return false;
}
for ( const elem of elems ) {
if ( elem.localName !== 'a' ) { continue; }
if ( elem.hasAttribute('href') === false ) { continue; }
const href = elem.getAttribute('href');
const text = extractURL(elem, source);
const hrefAfter = validateURL(text);
if ( hrefAfter === '' ) { continue; }
if ( hrefAfter === href ) { continue; }
elem.setAttribute('href', hrefAfter);
const count = sanitizeCopycats(href, hrefAfter);
safe.uboLog(logPrefix, `Sanitized ${count+1} links to\n${hrefAfter}`);
}
return true;
};
let observer, timer;
const onDomChanged = mutations => {
if ( timer !== undefined ) { return; }
let shouldSanitize = false;
for ( const mutation of mutations ) {
if ( mutation.addedNodes.length === 0 ) { continue; }
for ( const node of mutation.addedNodes ) {
if ( node.nodeType !== 1 ) { continue; }
shouldSanitize = true;
break;
}
if ( shouldSanitize ) { break; }
}
if ( shouldSanitize === false ) { return; }
timer = safe.onIdle(( ) => {
timer = undefined;
sanitize();
});
};
const start = ( ) => {
if ( sanitize() === false ) { return; }
observer = new MutationObserver(onDomChanged);
observer.observe(document.body, {
subtree: true,
childList: true,
});
};
runAt(( ) => { start(); }, 'interactive');
}
registerScriptlet(hrefSanitizer, {
name: 'href-sanitizer.js',
world: 'ISOLATED',
aliases: [
'urlskip.js',
],
dependencies: [
runAt,
safeSelf,
urlSkip,
],
});

View File

@@ -0,0 +1,235 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2019-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
*/
import { getSafeCookieValuesFn } from './cookie.js';
import { registerScriptlet } from './base.js';
import { safeSelf } from './safe-self.js';
/******************************************************************************/
export function getAllLocalStorageFn(which = 'localStorage') {
const storage = self[which];
const out = [];
for ( let i = 0; i < storage.length; i++ ) {
const key = storage.key(i);
const value = storage.getItem(key);
return { key, value };
}
return out;
}
registerScriptlet(getAllLocalStorageFn, {
name: 'get-all-local-storage.fn',
});
/******************************************************************************/
export function setLocalStorageItemFn(
which = 'local',
trusted = false,
key = '',
value = '',
) {
if ( key === '' ) { return; }
// For increased compatibility with AdGuard
if ( value === 'emptyArr' ) {
value = '[]';
} else if ( value === 'emptyObj' ) {
value = '{}';
}
const trustedValues = [
'',
'undefined', 'null',
'{}', '[]', '""',
'$remove$',
...getSafeCookieValuesFn(),
];
if ( trusted ) {
if ( value.includes('$now$') ) {
value = value.replaceAll('$now$', Date.now());
}
if ( value.includes('$currentDate$') ) {
value = value.replaceAll('$currentDate$', `${Date()}`);
}
if ( value.includes('$currentISODate$') ) {
value = value.replaceAll('$currentISODate$', (new Date()).toISOString());
}
} else {
const normalized = value.toLowerCase();
const match = /^("?)(.+)\1$/.exec(normalized);
const unquoted = match && match[2] || normalized;
if ( trustedValues.includes(unquoted) === false ) {
if ( /^-?\d+$/.test(unquoted) === false ) { return; }
const n = parseInt(unquoted, 10) || 0;
if ( n < -32767 || n > 32767 ) { return; }
}
}
try {
const storage = self[`${which}Storage`];
if ( value === '$remove$' ) {
const safe = safeSelf();
const pattern = safe.patternToRegex(key, undefined, true );
const toRemove = [];
for ( let i = 0, n = storage.length; i < n; i++ ) {
const key = storage.key(i);
if ( pattern.test(key) ) { toRemove.push(key); }
}
for ( const key of toRemove ) {
storage.removeItem(key);
}
} else {
storage.setItem(key, `${value}`);
}
} catch(ex) {
}
}
registerScriptlet(setLocalStorageItemFn, {
name: 'set-local-storage-item.fn',
dependencies: [
getSafeCookieValuesFn,
safeSelf,
],
});
/******************************************************************************/
export function removeCacheStorageItem(
cacheNamePattern = '',
requestPattern = ''
) {
if ( cacheNamePattern === '' ) { return; }
const safe = safeSelf();
const logPrefix = safe.makeLogPrefix('remove-cache-storage-item', cacheNamePattern, requestPattern);
const cacheStorage = self.caches;
if ( cacheStorage instanceof Object === false ) { return; }
const reCache = safe.patternToRegex(cacheNamePattern, undefined, true);
const reRequest = safe.patternToRegex(requestPattern, undefined, true);
cacheStorage.keys().then(cacheNames => {
for ( const cacheName of cacheNames ) {
if ( reCache.test(cacheName) === false ) { continue; }
if ( requestPattern === '' ) {
cacheStorage.delete(cacheName).then(result => {
if ( safe.logLevel > 1 ) {
safe.uboLog(logPrefix, `Deleting ${cacheName}`);
}
if ( result !== true ) { return; }
safe.uboLog(logPrefix, `Deleted ${cacheName}: ${result}`);
});
continue;
}
cacheStorage.open(cacheName).then(cache => {
cache.keys().then(requests => {
for ( const request of requests ) {
if ( reRequest.test(request.url) === false ) { continue; }
if ( safe.logLevel > 1 ) {
safe.uboLog(logPrefix, `Deleting ${cacheName}/${request.url}`);
}
cache.delete(request).then(result => {
if ( result !== true ) { return; }
safe.uboLog(logPrefix, `Deleted ${cacheName}/${request.url}: ${result}`);
});
}
});
});
}
});
}
registerScriptlet(removeCacheStorageItem, {
name: 'remove-cache-storage-item.fn',
world: 'ISOLATED',
dependencies: [
safeSelf,
],
});
/*******************************************************************************
*
* set-local-storage-item.js
* set-session-storage-item.js
*
* Set a local/session storage entry to a specific, allowed value.
*
* Reference:
* https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-local-storage-item.js
* https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-session-storage-item.js
*
**/
export function setLocalStorageItem(key = '', value = '') {
setLocalStorageItemFn('local', false, key, value);
}
registerScriptlet(setLocalStorageItem, {
name: 'set-local-storage-item.js',
world: 'ISOLATED',
dependencies: [
setLocalStorageItemFn,
],
});
export function setSessionStorageItem(key = '', value = '') {
setLocalStorageItemFn('session', false, key, value);
}
registerScriptlet(setSessionStorageItem, {
name: 'set-session-storage-item.js',
world: 'ISOLATED',
dependencies: [
setLocalStorageItemFn,
],
});
/*******************************************************************************
*
* trusted-set-local-storage-item.js
*
* Set a local storage entry to an arbitrary value.
*
* Reference:
* https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/trusted-set-local-storage-item.js
*
**/
export function trustedSetLocalStorageItem(key = '', value = '') {
setLocalStorageItemFn('local', true, key, value);
}
registerScriptlet(trustedSetLocalStorageItem, {
name: 'trusted-set-local-storage-item.js',
requiresTrust: true,
world: 'ISOLATED',
dependencies: [
setLocalStorageItemFn,
],
});
export function trustedSetSessionStorageItem(key = '', value = '') {
setLocalStorageItemFn('session', true, key, value);
}
registerScriptlet(trustedSetSessionStorageItem, {
name: 'trusted-set-session-storage-item.js',
requiresTrust: true,
world: 'ISOLATED',
dependencies: [
setLocalStorageItemFn,
],
});

View File

@@ -0,0 +1,54 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2019-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
*/
import { createArglistParser } from './shared.js';
import { registerScriptlet } from './base.js';
/******************************************************************************/
export function parseReplaceFn(s) {
if ( s.charCodeAt(0) !== 0x2F /* / */ ) { return; }
const parser = createArglistParser('/');
parser.nextArg(s, 1);
let pattern = s.slice(parser.argBeg, parser.argEnd);
if ( parser.transform ) {
pattern = parser.normalizeArg(pattern);
}
if ( pattern === '' ) { return; }
parser.nextArg(s, parser.separatorEnd);
let replacement = s.slice(parser.argBeg, parser.argEnd);
if ( parser.separatorEnd === parser.separatorBeg ) { return; }
if ( parser.transform ) {
replacement = parser.normalizeArg(replacement);
}
const flags = s.slice(parser.separatorEnd);
try {
return { re: new RegExp(pattern, flags), replacement };
} catch(_) {
}
}
registerScriptlet(parseReplaceFn, {
name: 'parse-replace.fn',
dependencies: [
createArglistParser,
],
});

View File

@@ -0,0 +1,236 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2019-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
*/
import { proxyApplyFn } from './proxy-apply.js';
import { registerScriptlet } from './base.js';
import { safeSelf } from './safe-self.js';
/******************************************************************************/
class RangeParser {
constructor(s) {
this.not = s.charAt(0) === '!';
if ( this.not ) { s = s.slice(1); }
if ( s === '' ) { return; }
const pos = s.indexOf('-');
if ( pos !== 0 ) {
this.min = this.max = parseInt(s, 10) || 0;
}
if ( pos !== -1 ) {
this.max = parseInt(s.slice(1), 10) || Number.MAX_SAFE_INTEGER;
}
}
unbound() {
return this.min === undefined && this.max === undefined;
}
test(v) {
const n = Math.min(Math.max(Number(v) || 0, 0), Number.MAX_SAFE_INTEGER);
if ( this.min === this.max ) {
return (this.min === undefined || n === this.min) !== this.not;
}
if ( this.min === undefined ) {
return (n <= this.max) !== this.not;
}
if ( this.max === undefined ) {
return (n >= this.min) !== this.not;
}
return (n >= this.min && n <= this.max) !== this.not;
}
}
registerScriptlet(RangeParser, {
name: 'range-parser.fn',
});
/**
* @scriptlet prevent-setTimeout
*
* @description
* Conditionally prevent execution of the callback function passed to native
* setTimeout method. With no parameters, all calls to setTimeout will be
* shown in the logger.
*
* @param [needle]
* A pattern to match against the stringified callback. The pattern can be a
* plain string, or a regex. Prepend with `!` to reverse the match condition.
*
* @param [delay]
* A value to match against the delay. Can be a single value for exact match,
* or a range:
* - `min-max`: matches if delay >= min and delay <= max
* - `min-`: matches if delay >= min
* - `-max`: matches if delay <= max
* No delay means to match any delay value.
* Prepend with `!` to reverse the match condition.
*
* */
export function preventSetTimeout(
needleRaw = '',
delayRaw = ''
) {
const safe = safeSelf();
const logPrefix = safe.makeLogPrefix('prevent-setTimeout', needleRaw, delayRaw);
const needleNot = needleRaw.charAt(0) === '!';
const reNeedle = safe.patternToRegex(needleNot ? needleRaw.slice(1) : needleRaw);
const range = new RangeParser(delayRaw);
proxyApplyFn('setTimeout', function(context) {
const { callArgs } = context;
const a = callArgs[0] instanceof Function
? String(safe.Function_toString(callArgs[0]))
: String(callArgs[0]);
const b = callArgs[1];
if ( needleRaw === '' && range.unbound() ) {
safe.uboLog(logPrefix, `Called:\n${a}\n${b}`);
return context.reflect();
}
if ( reNeedle.test(a) !== needleNot && range.test(b) ) {
callArgs[0] = function(){};
safe.uboLog(logPrefix, `Prevented:\n${a}\n${b}`);
}
return context.reflect();
});
}
registerScriptlet(preventSetTimeout, {
name: 'prevent-setTimeout.js',
aliases: [
'no-setTimeout-if.js',
'nostif.js',
'setTimeout-defuser.js',
],
dependencies: [
proxyApplyFn,
RangeParser,
safeSelf,
],
});
/**
* @scriptlet prevent-setInterval
*
* @description
* Conditionally prevent execution of the callback function passed to native
* setInterval method. With no parameters, all calls to setInterval will be
* shown in the logger.
*
* @param [needle]
* A pattern to match against the stringified callback. The pattern can be a
* plain string, or a regex. Prepend with `!` to reverse the match condition.
* No pattern means to match anything.
*
* @param [delay]
* A value to match against the delay. Can be a single value for exact match,
* or a range:
* - `min-max`: matches if delay >= min and delay <= max
* - `min-`: matches if delay >= min
* - `-max`: matches if delay <= max
* No delay means to match any delay value.
* Prepend with `!` to reverse the match condition.
*
* */
export function preventSetInterval(
needleRaw = '',
delayRaw = ''
) {
const safe = safeSelf();
const logPrefix = safe.makeLogPrefix('prevent-setInterval', needleRaw, delayRaw);
const needleNot = needleRaw.charAt(0) === '!';
const reNeedle = safe.patternToRegex(needleNot ? needleRaw.slice(1) : needleRaw);
const range = new RangeParser(delayRaw);
proxyApplyFn('setInterval', function(context) {
const { callArgs } = context;
const a = callArgs[0] instanceof Function
? String(safe.Function_toString(callArgs[0]))
: String(callArgs[0]);
const b = callArgs[1];
if ( needleRaw === '' && range.unbound() ) {
safe.uboLog(logPrefix, `Called:\n${a}\n${b}`);
return context.reflect();
}
if ( reNeedle.test(a) !== needleNot && range.test(b) ) {
callArgs[0] = function(){};
safe.uboLog(logPrefix, `Prevented:\n${a}\n${b}`);
}
return context.reflect();
});
}
registerScriptlet(preventSetInterval, {
name: 'prevent-setInterval.js',
aliases: [
'no-setInterval-if.js',
'nosiif.js',
'setInterval-defuser.js',
],
dependencies: [
proxyApplyFn,
RangeParser,
safeSelf,
],
});
/**
* @scriptlet prevent-requestAnimationFrame
*
* @description
* Conditionally prevent execution of the callback function passed to native
* requestAnimationFrame method. With no parameters, all calls to
* requestAnimationFrame will be shown in the logger.
*
* @param [needle]
* A pattern to match against the stringified callback. The pattern can be a
* plain string, or a regex.
* Prepend with `!` to reverse the match condition.
*
* */
export function preventRequestAnimationFrame(
needleRaw = ''
) {
const safe = safeSelf();
const logPrefix = safe.makeLogPrefix('prevent-requestAnimationFrame', needleRaw);
const needleNot = needleRaw.charAt(0) === '!';
const reNeedle = safe.patternToRegex(needleNot ? needleRaw.slice(1) : needleRaw);
proxyApplyFn('requestAnimationFrame', function(context) {
const { callArgs } = context;
const a = callArgs[0] instanceof Function
? String(safe.Function_toString(callArgs[0]))
: String(callArgs[0]);
if ( needleRaw === '' ) {
safe.uboLog(logPrefix, `Called:\n${a}`);
} else if ( reNeedle.test(a) !== needleNot ) {
callArgs[0] = function(){};
safe.uboLog(logPrefix, `Prevented:\n${a}`);
}
return context.reflect();
});
}
registerScriptlet(preventRequestAnimationFrame, {
name: 'prevent-requestAnimationFrame.js',
aliases: [
'no-requestAnimationFrame-if.js',
'norafif.js',
],
dependencies: [
proxyApplyFn,
safeSelf,
],
});

View File

@@ -0,0 +1,109 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2019-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
*/
import { registerScriptlet } from './base.js';
/******************************************************************************/
export function proxyApplyFn(
target = '',
handler = ''
) {
let context = globalThis;
let prop = target;
for (;;) {
const pos = prop.indexOf('.');
if ( pos === -1 ) { break; }
context = context[prop.slice(0, pos)];
if ( context instanceof Object === false ) { return; }
prop = prop.slice(pos+1);
}
const fn = context[prop];
if ( typeof fn !== 'function' ) { return; }
if ( proxyApplyFn.CtorContext === undefined ) {
proxyApplyFn.ctorContexts = [];
proxyApplyFn.CtorContext = class {
constructor(...args) {
this.init(...args);
}
init(callFn, callArgs) {
this.callFn = callFn;
this.callArgs = callArgs;
return this;
}
reflect() {
const r = Reflect.construct(this.callFn, this.callArgs);
this.callFn = this.callArgs = this.private = undefined;
proxyApplyFn.ctorContexts.push(this);
return r;
}
static factory(...args) {
return proxyApplyFn.ctorContexts.length !== 0
? proxyApplyFn.ctorContexts.pop().init(...args)
: new proxyApplyFn.CtorContext(...args);
}
};
proxyApplyFn.applyContexts = [];
proxyApplyFn.ApplyContext = class {
constructor(...args) {
this.init(...args);
}
init(callFn, thisArg, callArgs) {
this.callFn = callFn;
this.thisArg = thisArg;
this.callArgs = callArgs;
return this;
}
reflect() {
const r = Reflect.apply(this.callFn, this.thisArg, this.callArgs);
this.callFn = this.thisArg = this.callArgs = this.private = undefined;
proxyApplyFn.applyContexts.push(this);
return r;
}
static factory(...args) {
return proxyApplyFn.applyContexts.length !== 0
? proxyApplyFn.applyContexts.pop().init(...args)
: new proxyApplyFn.ApplyContext(...args);
}
};
}
const fnStr = fn.toString();
const toString = (function toString() { return fnStr; }).bind(null);
const proxyDetails = {
apply(target, thisArg, args) {
return handler(proxyApplyFn.ApplyContext.factory(target, thisArg, args));
},
get(target, prop) {
if ( prop === 'toString' ) { return toString; }
return Reflect.get(target, prop);
},
};
if ( fn.prototype?.constructor === fn ) {
proxyDetails.construct = function(target, args) {
return handler(proxyApplyFn.CtorContext.factory(target, args));
};
}
context[prop] = new Proxy(fn, proxyDetails);
}
registerScriptlet(proxyApplyFn, {
name: 'proxy-apply.fn',
});

View File

@@ -0,0 +1,120 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2019-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
*/
import { parseReplaceFn } from './parse-replace.js';
import { proxyApplyFn } from './proxy-apply.js';
import { registerScriptlet } from './base.js';
import { safeSelf } from './safe-self.js';
import { validateConstantFn } from './set-constant.js';
/**
* @scriptlet trusted-replace-argument.js
*
* @description
* Replace an argument passed to a method. Requires a trusted source.
*
* @param propChain
* The property chain to the function which argument must be replaced when
* called.
*
* @param argposRaw
* The zero-based position of the argument in the argument list. Use a negative
* number for a position relative to the last argument. Use literal `this` to
* replace the value used in `prototype`-based methods.
*
* @param argraw
* The replacement value, validated using the same heuristic as with the
* `set-constant.js` scriptlet.
* If the replacement value matches `json:...`, the value will be the
* json-parsed string after `json:`.
* If the replacement value matches `repl:/.../.../`, the target argument will
* be replaced according the regex-replacement directive following `repl:`
*
* @param [, condition, pattern]
* Optional. The replacement will occur only when pattern matches the target
* argument.
*
* */
export function trustedReplaceArgument(
propChain = '',
argposRaw = '',
argraw = ''
) {
if ( propChain === '' ) { return; }
const safe = safeSelf();
const logPrefix = safe.makeLogPrefix('trusted-replace-argument', propChain, argposRaw, argraw);
const argoffset = parseInt(argposRaw, 10) || 0;
const extraArgs = safe.getExtraArgs(Array.from(arguments), 3);
const replacer = argraw.startsWith('repl:/') &&
parseReplaceFn(argraw.slice(5)) || undefined;
const value = replacer === undefined &&
validateConstantFn(true, argraw, extraArgs) || undefined;
const reCondition = extraArgs.condition
? safe.patternToRegex(extraArgs.condition)
: /^/;
const getArg = context => {
if ( argposRaw === 'this' ) { return context.thisArg; }
const { callArgs } = context;
const argpos = argoffset >= 0 ? argoffset : callArgs.length - argoffset;
if ( argpos < 0 || argpos >= callArgs.length ) { return; }
context.private = { argpos };
return callArgs[argpos];
};
const setArg = (context, value) => {
if ( argposRaw === 'this' ) {
if ( value !== context.thisArg ) {
context.thisArg = value;
}
} else if ( context.private ) {
context.callArgs[context.private.argpos] = value;
}
};
proxyApplyFn(propChain, function(context) {
if ( argposRaw === '' ) {
safe.uboLog(logPrefix, `Arguments:\n${context.callArgs.join('\n')}`);
return context.reflect();
}
const argBefore = getArg(context);
if ( safe.RegExp_test.call(reCondition, argBefore) === false ) {
return context.reflect();
}
const argAfter = replacer && typeof argBefore === 'string'
? argBefore.replace(replacer.re, replacer.replacement)
: value;
if ( argAfter !== argBefore ) {
setArg(context, argAfter);
safe.uboLog(logPrefix, `Replaced argument:\nBefore: ${JSON.stringify(argBefore)}\nAfter: ${argAfter}`);
}
return context.reflect();
});
}
registerScriptlet(trustedReplaceArgument, {
name: 'trusted-replace-argument.js',
requiresTrust: true,
dependencies: [
parseReplaceFn,
proxyApplyFn,
safeSelf,
validateConstantFn,
],
});

View File

@@ -0,0 +1,96 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2019-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
*/
import { registerScriptlet } from './base.js';
import { safeSelf } from './safe-self.js';
/* eslint no-prototype-builtins: 0 */
/**
* @helperScriptlet run-at.fn
*
* @description
* Execute a function at a specific page-load milestone.
*
* @param fn
* The function to call.
*
* @param when
* An identifier which tells when the function should be executed.
* See <https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState>.
*
* @example
* `runAt(( ) => { start(); }, 'interactive')`
*
* */
export function runAt(fn, when) {
const intFromReadyState = state => {
const targets = {
'loading': 1, 'asap': 1,
'interactive': 2, 'end': 2, '2': 2,
'complete': 3, 'idle': 3, '3': 3,
};
const tokens = Array.isArray(state) ? state : [ state ];
for ( const token of tokens ) {
const prop = `${token}`;
if ( targets.hasOwnProperty(prop) === false ) { continue; }
return targets[prop];
}
return 0;
};
const runAt = intFromReadyState(when);
if ( intFromReadyState(document.readyState) >= runAt ) {
fn(); return;
}
const onStateChange = ( ) => {
if ( intFromReadyState(document.readyState) < runAt ) { return; }
fn();
safe.removeEventListener.apply(document, args);
};
const safe = safeSelf();
const args = [ 'readystatechange', onStateChange, { capture: true } ];
safe.addEventListener.apply(document, args);
}
registerScriptlet(runAt, {
name: 'run-at.fn',
dependencies: [
safeSelf,
],
});
/******************************************************************************/
export function runAtHtmlElementFn(fn) {
if ( document.documentElement ) {
fn();
return;
}
const observer = new MutationObserver(( ) => {
observer.disconnect();
fn();
});
observer.observe(document, { childList: true });
}
registerScriptlet(runAtHtmlElementFn, {
name: 'run-at-html-element.fn',
});

View File

@@ -0,0 +1,219 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2019-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
*/
import { registerScriptlet } from './base.js';
/******************************************************************************/
// Externally added to the private namespace in which scriptlets execute.
/* global scriptletGlobals */
export function safeSelf() {
if ( scriptletGlobals.safeSelf ) {
return scriptletGlobals.safeSelf;
}
const self = globalThis;
const safe = {
'Array_from': Array.from,
'Error': self.Error,
'Function_toStringFn': self.Function.prototype.toString,
'Function_toString': thisArg => safe.Function_toStringFn.call(thisArg),
'Math_floor': Math.floor,
'Math_max': Math.max,
'Math_min': Math.min,
'Math_random': Math.random,
'Object': Object,
'Object_defineProperty': Object.defineProperty.bind(Object),
'Object_defineProperties': Object.defineProperties.bind(Object),
'Object_fromEntries': Object.fromEntries.bind(Object),
'Object_getOwnPropertyDescriptor': Object.getOwnPropertyDescriptor.bind(Object),
'RegExp': self.RegExp,
'RegExp_test': self.RegExp.prototype.test,
'RegExp_exec': self.RegExp.prototype.exec,
'Request_clone': self.Request.prototype.clone,
'String_fromCharCode': String.fromCharCode,
'String_split': String.prototype.split,
'XMLHttpRequest': self.XMLHttpRequest,
'addEventListener': self.EventTarget.prototype.addEventListener,
'removeEventListener': self.EventTarget.prototype.removeEventListener,
'fetch': self.fetch,
'JSON': self.JSON,
'JSON_parseFn': self.JSON.parse,
'JSON_stringifyFn': self.JSON.stringify,
'JSON_parse': (...args) => safe.JSON_parseFn.call(safe.JSON, ...args),
'JSON_stringify': (...args) => safe.JSON_stringifyFn.call(safe.JSON, ...args),
'log': console.log.bind(console),
// Properties
logLevel: 0,
// Methods
makeLogPrefix(...args) {
return this.sendToLogger && `[${args.join(' \u205D ')}]` || '';
},
uboLog(...args) {
if ( this.sendToLogger === undefined ) { return; }
if ( args === undefined || args[0] === '' ) { return; }
return this.sendToLogger('info', ...args);
},
uboErr(...args) {
if ( this.sendToLogger === undefined ) { return; }
if ( args === undefined || args[0] === '' ) { return; }
return this.sendToLogger('error', ...args);
},
escapeRegexChars(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
},
initPattern(pattern, options = {}) {
if ( pattern === '' ) {
return { matchAll: true, expect: true };
}
const expect = (options.canNegate !== true || pattern.startsWith('!') === false);
if ( expect === false ) {
pattern = pattern.slice(1);
}
const match = /^\/(.+)\/([gimsu]*)$/.exec(pattern);
if ( match !== null ) {
return {
re: new this.RegExp(
match[1],
match[2] || options.flags
),
expect,
};
}
if ( options.flags !== undefined ) {
return {
re: new this.RegExp(this.escapeRegexChars(pattern),
options.flags
),
expect,
};
}
return { pattern, expect };
},
testPattern(details, haystack) {
if ( details.matchAll ) { return true; }
if ( details.re ) {
return this.RegExp_test.call(details.re, haystack) === details.expect;
}
return haystack.includes(details.pattern) === details.expect;
},
patternToRegex(pattern, flags = undefined, verbatim = false) {
if ( pattern === '' ) { return /^/; }
const match = /^\/(.+)\/([gimsu]*)$/.exec(pattern);
if ( match === null ) {
const reStr = this.escapeRegexChars(pattern);
return new RegExp(verbatim ? `^${reStr}$` : reStr, flags);
}
try {
return new RegExp(match[1], match[2] || undefined);
}
catch(ex) {
}
return /^/;
},
getExtraArgs(args, offset = 0) {
const entries = args.slice(offset).reduce((out, v, i, a) => {
if ( (i & 1) === 0 ) {
const rawValue = a[i+1];
const value = /^\d+$/.test(rawValue)
? parseInt(rawValue, 10)
: rawValue;
out.push([ a[i], value ]);
}
return out;
}, []);
return this.Object_fromEntries(entries);
},
onIdle(fn, options) {
if ( self.requestIdleCallback ) {
return self.requestIdleCallback(fn, options);
}
return self.requestAnimationFrame(fn);
},
offIdle(id) {
if ( self.requestIdleCallback ) {
return self.cancelIdleCallback(id);
}
return self.cancelAnimationFrame(id);
}
};
scriptletGlobals.safeSelf = safe;
if ( scriptletGlobals.bcSecret === undefined ) { return safe; }
// This is executed only when the logger is opened
safe.logLevel = scriptletGlobals.logLevel || 1;
let lastLogType = '';
let lastLogText = '';
let lastLogTime = 0;
safe.toLogText = (type, ...args) => {
if ( args.length === 0 ) { return; }
const text = `[${document.location.hostname || document.location.href}]${args.join(' ')}`;
if ( text === lastLogText && type === lastLogType ) {
if ( (Date.now() - lastLogTime) < 5000 ) { return; }
}
lastLogType = type;
lastLogText = text;
lastLogTime = Date.now();
return text;
};
try {
const bc = new self.BroadcastChannel(scriptletGlobals.bcSecret);
let bcBuffer = [];
safe.sendToLogger = (type, ...args) => {
const text = safe.toLogText(type, ...args);
if ( text === undefined ) { return; }
if ( bcBuffer === undefined ) {
return bc.postMessage({ what: 'messageToLogger', type, text });
}
bcBuffer.push({ type, text });
};
bc.onmessage = ev => {
const msg = ev.data;
switch ( msg ) {
case 'iamready!':
if ( bcBuffer === undefined ) { break; }
bcBuffer.forEach(({ type, text }) =>
bc.postMessage({ what: 'messageToLogger', type, text })
);
bcBuffer = undefined;
break;
case 'setScriptletLogLevelToOne':
safe.logLevel = 1;
break;
case 'setScriptletLogLevelToTwo':
safe.logLevel = 2;
break;
}
};
bc.postMessage('areyouready?');
} catch(_) {
safe.sendToLogger = (type, ...args) => {
const text = safe.toLogText(type, ...args);
if ( text === undefined ) { return; }
safe.log(`uBO ${text}`);
};
}
return safe;
}
registerScriptlet(safeSelf, {
name: 'safe-self.fn',
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,287 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2019-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
*/
import { registerScriptlet } from './base.js';
import { runAt } from './run-at.js';
import { safeSelf } from './safe-self.js';
/******************************************************************************/
export function validateConstantFn(trusted, raw, extraArgs = {}) {
const safe = safeSelf();
let value;
if ( raw === 'undefined' ) {
value = undefined;
} else if ( raw === 'false' ) {
value = false;
} else if ( raw === 'true' ) {
value = true;
} else if ( raw === 'null' ) {
value = null;
} else if ( raw === "''" || raw === '' ) {
value = '';
} else if ( raw === '[]' || raw === 'emptyArr' ) {
value = [];
} else if ( raw === '{}' || raw === 'emptyObj' ) {
value = {};
} else if ( raw === 'noopFunc' ) {
value = function(){};
} else if ( raw === 'trueFunc' ) {
value = function(){ return true; };
} else if ( raw === 'falseFunc' ) {
value = function(){ return false; };
} else if ( raw === 'throwFunc' ) {
value = function(){ throw ''; };
} else if ( /^-?\d+$/.test(raw) ) {
value = parseInt(raw);
if ( isNaN(raw) ) { return; }
if ( Math.abs(raw) > 0x7FFF ) { return; }
} else if ( trusted ) {
if ( raw.startsWith('json:') ) {
try { value = safe.JSON_parse(raw.slice(5)); } catch(ex) { return; }
} else if ( raw.startsWith('{') && raw.endsWith('}') ) {
try { value = safe.JSON_parse(raw).value; } catch(ex) { return; }
}
} else {
return;
}
if ( extraArgs.as !== undefined ) {
if ( extraArgs.as === 'function' ) {
return ( ) => value;
} else if ( extraArgs.as === 'callback' ) {
return ( ) => (( ) => value);
} else if ( extraArgs.as === 'resolved' ) {
return Promise.resolve(value);
} else if ( extraArgs.as === 'rejected' ) {
return Promise.reject(value);
}
}
return value;
}
registerScriptlet(validateConstantFn, {
name: 'validate-constant.fn',
dependencies: [
safeSelf,
],
});
/******************************************************************************/
export function setConstantFn(
trusted = false,
chain = '',
rawValue = ''
) {
if ( chain === '' ) { return; }
const safe = safeSelf();
const logPrefix = safe.makeLogPrefix('set-constant', chain, rawValue);
const extraArgs = safe.getExtraArgs(Array.from(arguments), 3);
function setConstant(chain, rawValue) {
const trappedProp = (( ) => {
const pos = chain.lastIndexOf('.');
if ( pos === -1 ) { return chain; }
return chain.slice(pos+1);
})();
const cloakFunc = fn => {
safe.Object_defineProperty(fn, 'name', { value: trappedProp });
return new Proxy(fn, {
defineProperty(target, prop) {
if ( prop !== 'toString' ) {
return Reflect.defineProperty(...arguments);
}
return true;
},
deleteProperty(target, prop) {
if ( prop !== 'toString' ) {
return Reflect.deleteProperty(...arguments);
}
return true;
},
get(target, prop) {
if ( prop === 'toString' ) {
return function() {
return `function ${trappedProp}() { [native code] }`;
}.bind(null);
}
return Reflect.get(...arguments);
},
});
};
if ( trappedProp === '' ) { return; }
const thisScript = document.currentScript;
let normalValue = validateConstantFn(trusted, rawValue, extraArgs);
if ( rawValue === 'noopFunc' || rawValue === 'trueFunc' || rawValue === 'falseFunc' ) {
normalValue = cloakFunc(normalValue);
}
let aborted = false;
const mustAbort = function(v) {
if ( trusted ) { return false; }
if ( aborted ) { return true; }
aborted =
(v !== undefined && v !== null) &&
(normalValue !== undefined && normalValue !== null) &&
(typeof v !== typeof normalValue);
if ( aborted ) {
safe.uboLog(logPrefix, `Aborted because value set to ${v}`);
}
return aborted;
};
// https://github.com/uBlockOrigin/uBlock-issues/issues/156
// Support multiple trappers for the same property.
const trapProp = function(owner, prop, configurable, handler) {
if ( handler.init(configurable ? owner[prop] : normalValue) === false ) { return; }
const odesc = safe.Object_getOwnPropertyDescriptor(owner, prop);
let prevGetter, prevSetter;
if ( odesc instanceof safe.Object ) {
owner[prop] = normalValue;
if ( odesc.get instanceof Function ) {
prevGetter = odesc.get;
}
if ( odesc.set instanceof Function ) {
prevSetter = odesc.set;
}
}
try {
safe.Object_defineProperty(owner, prop, {
configurable,
get() {
if ( prevGetter !== undefined ) {
prevGetter();
}
return handler.getter();
},
set(a) {
if ( prevSetter !== undefined ) {
prevSetter(a);
}
handler.setter(a);
}
});
safe.uboLog(logPrefix, 'Trap installed');
} catch(ex) {
safe.uboErr(logPrefix, ex);
}
};
const trapChain = function(owner, chain) {
const pos = chain.indexOf('.');
if ( pos === -1 ) {
trapProp(owner, chain, false, {
v: undefined,
init: function(v) {
if ( mustAbort(v) ) { return false; }
this.v = v;
return true;
},
getter: function() {
if ( document.currentScript === thisScript ) {
return this.v;
}
safe.uboLog(logPrefix, 'Property read');
return normalValue;
},
setter: function(a) {
if ( mustAbort(a) === false ) { return; }
normalValue = a;
}
});
return;
}
const prop = chain.slice(0, pos);
const v = owner[prop];
chain = chain.slice(pos + 1);
if ( v instanceof safe.Object || typeof v === 'object' && v !== null ) {
trapChain(v, chain);
return;
}
trapProp(owner, prop, true, {
v: undefined,
init: function(v) {
this.v = v;
return true;
},
getter: function() {
return this.v;
},
setter: function(a) {
this.v = a;
if ( a instanceof safe.Object ) {
trapChain(a, chain);
}
}
});
};
trapChain(window, chain);
}
runAt(( ) => {
setConstant(chain, rawValue);
}, extraArgs.runAt);
}
registerScriptlet(setConstantFn, {
name: 'set-constant.fn',
dependencies: [
runAt,
safeSelf,
validateConstantFn,
],
});
/******************************************************************************/
export function setConstant(
...args
) {
setConstantFn(false, ...args);
}
registerScriptlet(setConstant, {
name: 'set-constant.js',
aliases: [
'set.js',
],
dependencies: [
setConstantFn,
],
});
/*******************************************************************************
*
* trusted-set-constant.js
*
* Set specified property to any value. This is essentially the same as
* set-constant.js, but with no restriction as to which values can be used.
*
**/
export function trustedSetConstant(
...args
) {
setConstantFn(true, ...args);
}
registerScriptlet(trustedSetConstant, {
name: 'trusted-set-constant.js',
requiresTrust: true,
aliases: [
'trusted-set.js',
],
dependencies: [
setConstantFn,
],
});

View File

@@ -0,0 +1,44 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2019-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
*/
// Code imported from main code base and exposed as injectable scriptlets
import { ArglistParser } from '../arglist-parser.js';
import { registerScriptlet } from './base.js';
/******************************************************************************/
registerScriptlet(ArglistParser, {
name: 'arglist-parser.fn',
});
/******************************************************************************/
export function createArglistParser(...args) {
return new ArglistParser(...args);
}
registerScriptlet(createArglistParser, {
name: 'create-arglist-parser.fn',
dependencies: [
ArglistParser,
],
});

View File

@@ -0,0 +1,163 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2019-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
*/
import { registerScriptlet } from './base.js';
import { safeSelf } from './safe-self.js';
/**
* @scriptlet spoof-css.js
*
* @description
* Spoof the value of CSS properties.
*
* @param selector
* A CSS selector for the element(s) to target.
*
* @param [property, value, ...]
* A list of property-value pairs of the style properties to spoof to the
* specified values.
*
* */
export function spoofCSS(
selector,
...args
) {
if ( typeof selector !== 'string' ) { return; }
if ( selector === '' ) { return; }
const toCamelCase = s => s.replace(/-[a-z]/g, s => s.charAt(1).toUpperCase());
const propToValueMap = new Map();
const privatePropToValueMap = new Map();
for ( let i = 0; i < args.length; i += 2 ) {
const prop = toCamelCase(args[i+0]);
if ( prop === '' ) { break; }
const value = args[i+1];
if ( typeof value !== 'string' ) { break; }
if ( prop.charCodeAt(0) === 0x5F /* _ */ ) {
privatePropToValueMap.set(prop, value);
} else {
propToValueMap.set(prop, value);
}
}
const safe = safeSelf();
const logPrefix = safe.makeLogPrefix('spoof-css', selector, ...args);
const instanceProperties = [ 'cssText', 'length', 'parentRule' ];
const spoofStyle = (prop, real) => {
const normalProp = toCamelCase(prop);
const shouldSpoof = propToValueMap.has(normalProp);
const value = shouldSpoof ? propToValueMap.get(normalProp) : real;
if ( shouldSpoof ) {
safe.uboLog(logPrefix, `Spoofing ${prop} to ${value}`);
}
return value;
};
const cloackFunc = (fn, thisArg, name) => {
const trap = fn.bind(thisArg);
Object.defineProperty(trap, 'name', { value: name });
Object.defineProperty(trap, 'toString', {
value: ( ) => `function ${name}() { [native code] }`
});
return trap;
};
self.getComputedStyle = new Proxy(self.getComputedStyle, {
apply: function(target, thisArg, args) {
// eslint-disable-next-line no-debugger
if ( privatePropToValueMap.has('_debug') ) { debugger; }
const style = Reflect.apply(target, thisArg, args);
const targetElements = new WeakSet(document.querySelectorAll(selector));
if ( targetElements.has(args[0]) === false ) { return style; }
const proxiedStyle = new Proxy(style, {
get(target, prop) {
if ( typeof target[prop] === 'function' ) {
if ( prop === 'getPropertyValue' ) {
return cloackFunc(function getPropertyValue(prop) {
return spoofStyle(prop, target[prop]);
}, target, 'getPropertyValue');
}
return cloackFunc(target[prop], target, prop);
}
if ( instanceProperties.includes(prop) ) {
return Reflect.get(target, prop);
}
return spoofStyle(prop, Reflect.get(target, prop));
},
getOwnPropertyDescriptor(target, prop) {
if ( propToValueMap.has(prop) ) {
return {
configurable: true,
enumerable: true,
value: propToValueMap.get(prop),
writable: true,
};
}
return Reflect.getOwnPropertyDescriptor(target, prop);
},
});
return proxiedStyle;
},
get(target, prop) {
if ( prop === 'toString' ) {
return target.toString.bind(target);
}
return Reflect.get(target, prop);
},
});
Element.prototype.getBoundingClientRect = new Proxy(Element.prototype.getBoundingClientRect, {
apply: function(target, thisArg, args) {
// eslint-disable-next-line no-debugger
if ( privatePropToValueMap.has('_debug') ) { debugger; }
const rect = Reflect.apply(target, thisArg, args);
const targetElements = new WeakSet(document.querySelectorAll(selector));
if ( targetElements.has(thisArg) === false ) { return rect; }
let { x, y, height, width } = rect;
if ( privatePropToValueMap.has('_rectx') ) {
x = parseFloat(privatePropToValueMap.get('_rectx'));
}
if ( privatePropToValueMap.has('_recty') ) {
y = parseFloat(privatePropToValueMap.get('_recty'));
}
if ( privatePropToValueMap.has('_rectw') ) {
width = parseFloat(privatePropToValueMap.get('_rectw'));
} else if ( propToValueMap.has('width') ) {
width = parseFloat(propToValueMap.get('width'));
}
if ( privatePropToValueMap.has('_recth') ) {
height = parseFloat(privatePropToValueMap.get('_recth'));
} else if ( propToValueMap.has('height') ) {
height = parseFloat(propToValueMap.get('height'));
}
return new self.DOMRect(x, y, width, height);
},
get(target, prop) {
if ( prop === 'toString' ) {
return target.toString.bind(target);
}
return Reflect.get(target, prop);
},
});
}
registerScriptlet(spoofCSS, {
name: 'spoof-css.js',
dependencies: [
safeSelf,
],
});

View File

@@ -0,0 +1,287 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2015-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';
/******************************************************************************/
let listEntries = Object.create(null);
/******************************************************************************/
// https://github.com/uBlockOrigin/uBlock-issues/issues/2092
// Order of ids matters
const extractBlocks = function(content, ...ids) {
const out = [];
for ( const id of ids ) {
const pattern = `#block-start-${id}\n`;
let beg = content.indexOf(pattern);
if ( beg === -1 ) { continue; }
beg += pattern.length;
const end = content.indexOf(`#block-end-${id}`, beg);
out.push(content.slice(beg, end));
}
return out.join('\n');
};
/******************************************************************************/
// https://github.com/MajkiIT/polish-ads-filter/issues/14768#issuecomment-536006312
// Avoid reporting badfilter-ed filters.
const fromNetFilter = function(details) {
const lists = [];
const compiledFilter = details.compiledFilter;
for ( const assetKey in listEntries ) {
const entry = listEntries[assetKey];
if ( entry === undefined ) { continue; }
if ( entry.networkContent === undefined ) {
entry.networkContent = extractBlocks(entry.content, 'NETWORK_FILTERS:GOOD');
}
const content = entry.networkContent;
let pos = 0;
for (;;) {
pos = content.indexOf(compiledFilter, pos);
if ( pos === -1 ) { break; }
// We need an exact match.
// https://github.com/gorhill/uBlock/issues/1392
// https://github.com/gorhill/uBlock/issues/835
const notFound = pos !== 0 && content.charCodeAt(pos - 1) !== 0x0A;
pos += compiledFilter.length;
if (
notFound ||
pos !== content.length && content.charCodeAt(pos) !== 0x0A
) {
continue;
}
lists.push({
assetKey: assetKey,
title: entry.title,
supportURL: entry.supportURL
});
break;
}
}
const response = {};
response[details.rawFilter] = lists;
self.postMessage({ id: details.id, response });
};
/******************************************************************************/
// Looking up filter lists from a cosmetic filter is a bit more complicated
// than with network filters:
//
// The filter is its raw representation, not its compiled version. This is
// because the cosmetic filtering engine can't translate a live cosmetic
// filter into its compiled version. Reason is I do not want to burden
// cosmetic filtering with the resource overhead of being able to recompile
// live cosmetic filters. I want the cosmetic filtering code to be left
// completely unaffected by reverse lookup requirements.
//
// Mainly, given a CSS selector and a hostname as context, we will derive
// various versions of compiled filters and see if there are matches. This
// way the whole CPU cost is incurred by the reverse lookup code -- in a
// worker thread, and the cosmetic filtering engine incurs no cost at all.
//
// For this though, the reverse lookup code here needs some knowledge of
// the inners of the cosmetic filtering engine.
// FilterContainer.fromCompiledContent() is our reference code to create
// the various compiled versions.
const fromExtendedFilter = function(details) {
const match = /^#@?#\^?/.exec(details.rawFilter);
const prefix = match[0];
const exception = prefix.charAt(1) === '@';
const selector = details.rawFilter.slice(prefix.length);
const isHtmlFilter = prefix.endsWith('^');
const hostname = details.hostname;
// The longer the needle, the lower the number of false positives.
// https://github.com/uBlockOrigin/uBlock-issues/issues/1139
// Mind that there is no guarantee a selector has `\w` characters.
const needle = selector.match(/\w+|\*/g).reduce(function(a, b) {
return a.length > b.length ? a : b;
});
const regexFromLabels = (prefix, hn, suffix) =>
new RegExp(
prefix +
hn.split('.').reduce((acc, item) => `(${acc}\\.)?${item}`) +
suffix
);
// https://github.com/uBlockOrigin/uBlock-issues/issues/803
// Support looking up selectors of the form `*##...`
const reHostname = regexFromLabels('^', hostname, '$');
let reEntity;
{
const domain = details.domain;
const pos = domain.indexOf('.');
if ( pos !== -1 ) {
reEntity = regexFromLabels(
'^(',
hostname.slice(0, pos + hostname.length - domain.length),
'\\.)?\\*$'
);
}
}
const hostnameMatches = hn => {
if ( hn === '' ) { return true; }
if ( hn.charCodeAt(0) === 0x2F /* / */ ) {
return (new RegExp(hn.slice(1,-1))).test(hostname);
}
if ( reHostname.test(hn) ) { return true; }
if ( reEntity === undefined ) { return false; }
if ( reEntity.test(hn) ) { return true; }
return false;
};
const response = Object.create(null);
for ( const assetKey in listEntries ) {
const entry = listEntries[assetKey];
if ( entry === undefined ) { continue; }
if ( entry.extendedContent === undefined ) {
entry.extendedContent = extractBlocks(
entry.content,
'COSMETIC_FILTERS:SPECIFIC',
'COSMETIC_FILTERS:GENERIC',
'SCRIPTLET_FILTERS',
'HTML_FILTERS',
'HTTPHEADER_FILTERS'
);
}
const content = entry.extendedContent;
let found;
let pos = 0;
while ( (pos = content.indexOf(needle, pos)) !== -1 ) {
let beg = content.lastIndexOf('\n', pos);
if ( beg === -1 ) { beg = 0; }
let end = content.indexOf('\n', pos);
if ( end === -1 ) { end = content.length; }
pos = end;
const fargs = JSON.parse(content.slice(beg, end));
const filterType = fargs[0];
// https://github.com/gorhill/uBlock/issues/2763
if ( filterType === 0 && details.ignoreGeneric ) { continue; }
// Do not confuse cosmetic filters with HTML ones.
if ( (filterType === 64) !== isHtmlFilter ) { continue; }
switch ( filterType ) {
// Lowly generic cosmetic filters
case 0:
if ( exception ) { break; }
if ( fargs[2] !== selector ) { break; }
found = prefix + selector;
break;
// Highly generic cosmetic filters
case 4: // simple highly generic
case 5: // complex highly generic
if ( exception ) { break; }
if ( fargs[1] !== selector ) { break; }
found = prefix + selector;
break;
// Specific cosmetic filtering
// Generic exception
case 8:
// HTML filtering
// Response header filtering
case 64: {
if ( exception !== ((fargs[2] & 0b001) !== 0) ) { break; }
const isProcedural = (fargs[2] & 0b010) !== 0;
if (
isProcedural === false && fargs[3] !== selector ||
isProcedural && JSON.parse(fargs[3]).raw !== selector
) {
break;
}
if ( hostnameMatches(fargs[1]) === false ) { break; }
// https://www.reddit.com/r/uBlockOrigin/comments/d6vxzj/
// Ignore match if specific cosmetic filters are disabled
if (
filterType === 8 &&
exception === false &&
details.ignoreSpecific
) {
break;
}
found = fargs[1] + prefix + selector;
break;
}
// Scriptlet injection
case 32:
if ( exception !== ((fargs[2] & 0b001) !== 0) ) { break; }
if ( fargs[3] !== details.compiled ) { break; }
if ( hostnameMatches(fargs[1]) ) {
found = fargs[1] + prefix + selector;
}
break;
}
if ( found !== undefined ) {
if ( response[found] === undefined ) {
response[found] = [];
}
response[found].push({
assetKey: assetKey,
title: entry.title,
supportURL: entry.supportURL
});
break;
}
}
}
self.postMessage({ id: details.id, response });
};
/******************************************************************************/
self.onmessage = function(e) {
const msg = e.data;
switch ( msg.what ) {
case 'resetLists':
listEntries = Object.create(null);
break;
case 'setList':
listEntries[msg.details.assetKey] = msg.details;
break;
case 'fromNetFilter':
fromNetFilter(msg);
break;
case 'fromExtendedFilter':
fromExtendedFilter(msg);
break;
}
};
/******************************************************************************/

View File

@@ -0,0 +1,223 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2015-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';
/******************************************************************************/
import staticNetFilteringEngine from './static-net-filtering.js';
import µb from './background.js';
import { CompiledListWriter } from './static-filtering-io.js';
import { i18n$ } from './i18n.js';
import * as sfp from './static-filtering-parser.js';
import {
domainFromHostname,
hostnameFromURI,
} from './uri-utils.js';
/******************************************************************************/
const pendingResponses = new Map();
let worker = null;
let needLists = true;
let messageId = 1;
const onWorkerMessage = function(e) {
const msg = e.data;
const resolver = pendingResponses.get(msg.id);
pendingResponses.delete(msg.id);
resolver(msg.response);
};
const stopWorker = function() {
workerTTLTimer.off();
if ( worker === null ) { return; }
worker.terminate();
worker = null;
needLists = true;
for ( const resolver of pendingResponses.values() ) {
resolver();
}
pendingResponses.clear();
};
const workerTTLTimer = vAPI.defer.create(stopWorker);
const workerTTL = { min: 1.5 };
const initWorker = function() {
if ( worker === null ) {
worker = new Worker('js/reverselookup-worker.js');
worker.onmessage = onWorkerMessage;
}
// The worker will be shutdown after n minutes without being used.
workerTTLTimer.offon(workerTTL);
if ( needLists === false ) {
return Promise.resolve();
}
needLists = false;
const entries = new Map();
const onListLoaded = function(details) {
const entry = entries.get(details.assetKey);
// https://github.com/gorhill/uBlock/issues/536
// Use assetKey when there is no filter list title.
worker.postMessage({
what: 'setList',
details: {
assetKey: details.assetKey,
title: entry.title || details.assetKey,
supportURL: entry.supportURL,
content: details.content
}
});
};
for ( const listKey in µb.availableFilterLists ) {
if ( µb.availableFilterLists.hasOwnProperty(listKey) === false ) {
continue;
}
const entry = µb.availableFilterLists[listKey];
if ( entry.off === true ) { continue; }
entries.set(listKey, {
title: listKey !== µb.userFiltersPath ?
entry.title :
i18n$('1pPageName'),
supportURL: entry.supportURL || ''
});
}
if ( entries.size === 0 ) {
return Promise.resolve();
}
const promises = [];
for ( const listKey of entries.keys() ) {
promises.push(
µb.getCompiledFilterList(listKey).then(details => {
onListLoaded(details);
})
);
}
return Promise.all(promises);
};
const fromNetFilter = async function(rawFilter) {
if ( typeof rawFilter !== 'string' || rawFilter === '' ) { return; }
const writer = new CompiledListWriter();
const parser = new sfp.AstFilterParser({
trustedSource: true,
maxTokenLength: staticNetFilteringEngine.MAX_TOKEN_LENGTH,
nativeCssHas: vAPI.webextFlavor.env.includes('native_css_has'),
});
parser.parse(rawFilter);
const compiler = staticNetFilteringEngine.createCompiler();
if ( compiler.compile(parser, writer) === false ) { return; }
await initWorker();
const id = messageId++;
worker.postMessage({
what: 'fromNetFilter',
id,
compiledFilter: writer.last(),
rawFilter,
});
return new Promise(resolve => {
pendingResponses.set(id, resolve);
});
};
const fromExtendedFilter = async function(details) {
if (
typeof details.rawFilter !== 'string' ||
details.rawFilter === ''
) {
return;
}
await initWorker();
const id = messageId++;
const hostname = hostnameFromURI(details.url);
const parser = new sfp.AstFilterParser({
trustedSource: true,
nativeCssHas: vAPI.webextFlavor.env.includes('native_css_has'),
});
parser.parse(details.rawFilter);
let compiled;
if ( parser.isScriptletFilter() ) {
compiled = JSON.stringify(parser.getScriptletArgs());
}
worker.postMessage({
what: 'fromExtendedFilter',
id,
domain: domainFromHostname(hostname),
hostname,
ignoreGeneric:
staticNetFilteringEngine.matchRequestReverse(
'generichide',
details.url
) === 2,
ignoreSpecific:
staticNetFilteringEngine.matchRequestReverse(
'specifichide',
details.url
) === 2,
rawFilter: details.rawFilter,
compiled,
});
return new Promise(resolve => {
pendingResponses.set(id, resolve);
});
};
// This tells the worker that filter lists may have changed.
const resetLists = function() {
needLists = true;
if ( worker === null ) { return; }
worker.postMessage({ what: 'resetLists' });
};
/******************************************************************************/
const staticFilteringReverseLookup = {
fromNetFilter,
fromExtendedFilter,
resetLists,
shutdown: stopWorker
};
export default staticFilteringReverseLookup;
/******************************************************************************/

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,302 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2017-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';
/******************************************************************************/
import { redirectEngine as reng } from './redirect-engine.js';
import { StaticExtFilteringHostnameDB } from './static-ext-filtering-db.js';
/******************************************************************************/
// Increment when internal representation changes
const VERSION = 1;
const $scriptlets = new Set();
const $exceptions = new Set();
const $mainWorldMap = new Map();
const $isolatedWorldMap = new Map();
/******************************************************************************/
const normalizeRawFilter = (parser, sourceIsTrusted = false) => {
const args = parser.getScriptletArgs();
if ( args.length !== 0 ) {
let token = `${args[0]}.js`;
if ( reng.aliases.has(token) ) {
token = reng.aliases.get(token);
}
if ( parser.isException() !== true ) {
if ( sourceIsTrusted !== true ) {
if ( reng.tokenRequiresTrust(token) ) { return; }
}
}
args[0] = token.slice(0, -3);
}
return JSON.stringify(args);
};
const lookupScriptlet = (rawToken, mainMap, isolatedMap, debug = false) => {
if ( mainMap.has(rawToken) || isolatedMap.has(rawToken) ) { return; }
const args = JSON.parse(rawToken);
const token = `${args[0]}.js`;
const details = reng.contentFromName(token, 'text/javascript');
if ( details === undefined ) { return; }
const targetWorldMap = details.world !== 'ISOLATED' ? mainMap : isolatedMap;
const content = patchScriptlet(details.js, args.slice(1));
const dependencies = details.dependencies || [];
while ( dependencies.length !== 0 ) {
const token = dependencies.shift();
if ( targetWorldMap.has(token) ) { continue; }
const details = reng.contentFromName(token, 'fn/javascript') ||
reng.contentFromName(token, 'text/javascript');
if ( details === undefined ) { continue; }
targetWorldMap.set(token, details.js);
if ( Array.isArray(details.dependencies) === false ) { continue; }
dependencies.push(...details.dependencies);
}
targetWorldMap.set(rawToken, [
'try {',
'// >>>> scriptlet start',
content,
'// <<<< scriptlet end',
'} catch (e) {',
debug ? 'console.error(e);' : '',
'}',
].join('\n'));
};
// Fill-in scriptlet argument placeholders.
const patchScriptlet = (content, arglist) => {
if ( content.startsWith('function') && content.endsWith('}') ) {
content = `(${content})({{args}});`;
}
for ( let i = 0; i < arglist.length; i++ ) {
content = content.replace(`{{${i+1}}}`, arglist[i]);
}
return content.replace('{{args}}',
JSON.stringify(arglist).slice(1,-1).replace(/\$/g, '$$$')
);
};
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);
if ( args.length === 0 ) { return '+js()'; }
return `+js(${args.map(s => requote(s)).join(', ')})`;
};
/******************************************************************************/
export class ScriptletFilteringEngine {
constructor() {
this.acceptedCount = 0;
this.discardedCount = 0;
this.scriptletDB = new StaticExtFilteringHostnameDB(1, VERSION);
this.duplicates = new Set();
}
getFilterCount() {
return this.scriptletDB.size;
}
reset() {
this.scriptletDB.clear();
this.duplicates.clear();
this.acceptedCount = 0;
this.discardedCount = 0;
}
freeze() {
this.duplicates.clear();
this.scriptletDB.collectGarbage();
}
// parser: instance of AstFilterParser from static-filtering-parser.js
// writer: instance of CompiledListWriter from static-filtering-io.js
compile(parser, writer) {
writer.select('SCRIPTLET_FILTERS');
// Only exception filters are allowed to be global.
const isException = parser.isException();
const normalized = normalizeRawFilter(parser, writer.properties.get('trustedSource'));
// Can fail if there is a mismatch with trust requirement
if ( normalized === undefined ) { return; }
// Tokenless is meaningful only for exception filters.
if ( normalized === '[]' && isException === false ) { return; }
if ( parser.hasOptions() === false ) {
if ( isException ) {
writer.push([ 32, '', 1, normalized ]);
}
return;
}
// https://github.com/gorhill/uBlock/issues/3375
// Ignore instances of exception filter with negated hostnames,
// because there is no way to create an exception to an exception.
for ( const { hn, not, bad } of parser.getExtFilterDomainIterator() ) {
if ( bad ) { continue; }
let kind = 0;
if ( isException ) {
if ( not ) { continue; }
kind |= 1;
} else if ( not ) {
kind |= 1;
}
writer.push([ 32, hn, kind, normalized ]);
}
}
// writer: instance of CompiledListReader from static-filtering-io.js
fromCompiledContent(reader) {
reader.select('SCRIPTLET_FILTERS');
while ( reader.next() ) {
this.acceptedCount += 1;
const fingerprint = reader.fingerprint();
if ( this.duplicates.has(fingerprint) ) {
this.discardedCount += 1;
continue;
}
this.duplicates.add(fingerprint);
const args = reader.args();
if ( args.length < 4 ) { continue; }
this.scriptletDB.store(args[1], args[2], args[3]);
}
}
toSelfie() {
return this.scriptletDB.toSelfie();
}
fromSelfie(selfie) {
if ( typeof selfie !== 'object' || selfie === null ) { return false; }
if ( selfie.version !== VERSION ) { return false; }
this.scriptletDB.fromSelfie(selfie);
return true;
}
retrieve(request, options = {}) {
if ( this.scriptletDB.size === 0 ) { return; }
$scriptlets.clear();
$exceptions.clear();
const { hostname } = request;
this.scriptletDB.retrieve(hostname, [ $scriptlets, $exceptions ]);
const entity = request.entity !== ''
? `${hostname.slice(0, -request.domain.length)}${request.entity}`
: '*';
this.scriptletDB.retrieve(entity, [ $scriptlets, $exceptions ], 1);
if ( $scriptlets.size === 0 ) { return; }
// Wholly disable scriptlet injection?
if ( $exceptions.has('[]') ) {
return { filters: '#@#+js()' };
}
for ( const token of $exceptions ) {
if ( $scriptlets.has(token) ) {
$scriptlets.delete(token);
} else {
$exceptions.delete(token);
}
}
for ( const token of $scriptlets ) {
lookupScriptlet(token, $mainWorldMap, $isolatedWorldMap, options.debug);
}
const mainWorldCode = [];
for ( const js of $mainWorldMap.values() ) {
mainWorldCode.push(js);
}
const isolatedWorldCode = [];
for ( const js of $isolatedWorldMap.values() ) {
isolatedWorldCode.push(js);
}
const scriptletDetails = {
mainWorld: mainWorldCode.join('\n\n'),
isolatedWorld: isolatedWorldCode.join('\n\n'),
filters: [
...Array.from($scriptlets).map(s => `##${decompile(s)}`),
...Array.from($exceptions).map(s => `#@#${decompile(s)}`),
].join('\n'),
};
$mainWorldMap.clear();
$isolatedWorldMap.clear();
const scriptletGlobals = options.scriptletGlobals || {};
if ( options.debug ) {
scriptletGlobals.canDebug = true;
}
return {
mainWorld: scriptletDetails.mainWorld === '' ? '' : [
'(function() {',
'// >>>> start of private namespace',
'',
options.debugScriptlets ? 'debugger;' : ';',
'',
// For use by scriptlets to share local data among themselves
`const scriptletGlobals = ${JSON.stringify(scriptletGlobals, null, 4)}`,
'',
scriptletDetails.mainWorld,
'',
'// <<<< end of private namespace',
'})();',
].join('\n'),
isolatedWorld: scriptletDetails.isolatedWorld === '' ? '' : [
'function() {',
'// >>>> start of private namespace',
'',
options.debugScriptlets ? 'debugger;' : ';',
'',
// For use by scriptlets to share local data among themselves
`const scriptletGlobals = ${JSON.stringify(scriptletGlobals, null, 4)}`,
'',
scriptletDetails.isolatedWorld,
'',
'// <<<< end of private namespace',
'}',
].join('\n'),
filters: scriptletDetails.filters,
};
}
}
/******************************************************************************/

View File

@@ -0,0 +1,394 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2017-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
*/
/******************************************************************************/
import {
domainFromHostname,
entityFromDomain,
hostnameFromURI,
} from './uri-utils.js';
import { MRUCache } from './mrucache.js';
import { ScriptletFilteringEngine } from './scriptlet-filtering-core.js';
import logger from './logger.js';
import { onBroadcast } from './broadcast.js';
import { redirectEngine as reng } from './redirect-engine.js';
import { sessionFirewall } from './filtering-engines.js';
import µb from './background.js';
/******************************************************************************/
const contentScriptRegisterer = new (class {
constructor() {
this.hostnameToDetails = new Map();
}
register(hostname, code) {
if ( browser.contentScripts === undefined ) { return false; }
if ( hostname === '' ) { return false; }
const details = this.hostnameToDetails.get(hostname);
if ( details !== undefined ) {
if ( code === details.code ) {
return details.handle instanceof Promise === false;
}
details.handle.unregister();
this.hostnameToDetails.delete(hostname);
}
const promise = browser.contentScripts.register({
js: [ { code } ],
allFrames: true,
matches: [ `*://*.${hostname}/*` ],
matchAboutBlank: true,
runAt: 'document_start',
}).then(handle => {
this.hostnameToDetails.set(hostname, { handle, code });
return handle;
}).catch(( ) => {
this.hostnameToDetails.delete(hostname);
});
this.hostnameToDetails.set(hostname, { handle: promise, code });
return false;
}
unregister(hostname) {
if ( hostname === '' ) { return; }
if ( this.hostnameToDetails.size === 0 ) { return; }
const details = this.hostnameToDetails.get(hostname);
if ( details === undefined ) { return; }
this.hostnameToDetails.delete(hostname);
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; }
const pos = hn.length - hostname.length;
if ( pos !== 0 && hn.charCodeAt(pos-1) !== 0x2E /* . */ ) { continue; }
this.unregister(hn);
}
}
reset() {
if ( this.hostnameToDetails.size === 0 ) { return; }
for ( const details of this.hostnameToDetails.values() ) {
this.unregisterHandle(details.handle);
}
this.hostnameToDetails.clear();
}
unregisterHandle(handle) {
if ( handle instanceof Promise ) {
handle.then(handle => {
if ( handle ) { handle.unregister(); }
});
} else {
handle.unregister();
}
}
})();
/******************************************************************************/
const isolatedWorldInjector = (( ) => {
const parts = [
'(',
function(details) {
if ( self.uBO_isolatedScriptlets === 'done' ) { return; }
const doc = document;
if ( doc.location === null ) { return; }
const hostname = doc.location.hostname;
if ( hostname !== '' && details.hostname !== hostname ) { return; }
const isolatedScriptlets = function(){};
isolatedScriptlets();
self.uBO_isolatedScriptlets = 'done';
return 0;
}.toString(),
')(',
'json-slot',
');',
];
const jsonSlot = parts.indexOf('json-slot');
return {
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) +
details.isolatedWorld +
code.slice(match.index + match[0].length);
},
};
})();
const onScriptletMessageInjector = (( ) => {
const parts = [
'(',
function(name) {
if ( self.uBO_bcSecret ) { return; }
try {
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;
} catch(_) {
}
}.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 {
constructor() {
super();
this.warOrigin = vAPI.getURL('/web_accessible_resources');
this.warSecret = undefined;
this.scriptletCache = new MRUCache(32);
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;
}
});
}
reset() {
super.reset();
this.warSecret = vAPI.warSecret.long(this.warSecret);
this.clearCache();
}
freeze() {
super.freeze();
this.warSecret = vAPI.warSecret.long(this.warSecret);
this.clearCache();
}
clearCache() {
this.scriptletCache.reset();
contentScriptRegisterer.reset();
}
retrieve(request) {
const { hostname } = request;
// https://github.com/gorhill/uBlock/issues/2835
// Do not inject scriptlets if the site is under an `allow` rule.
if ( µb.userSettings.advancedUserEnabled ) {
if ( sessionFirewall.evaluateCellZY(hostname, hostname, '*') === 2 ) {
return;
}
}
if ( this.scriptletCache.resetTime < reng.modifyTime ) {
this.clearCache();
}
let scriptletDetails = this.scriptletCache.lookup(hostname);
if ( scriptletDetails !== undefined ) {
return scriptletDetails || undefined;
}
if ( this.isDevBuild === undefined ) {
this.isDevBuild = vAPI.webextFlavor.soup.has('devbuild') ||
µb.hiddenSettings.filterAuthorMode;
}
if ( this.warSecret === undefined ) {
this.warSecret = vAPI.warSecret.long();
}
const bcSecret = vAPI.generateSecret(3);
const options = {
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);
if ( scriptletDetails === undefined ) {
if ( request.nocache !== true ) {
this.scriptletCache.add(hostname, null);
}
return;
}
const contentScript = [];
if ( scriptletDetails.mainWorld ) {
contentScript.push(vAPI.scriptletsInjector(hostname, scriptletDetails));
}
if ( scriptletDetails.isolatedWorld ) {
contentScript.push(isolatedWorldInjector.assemble(hostname, scriptletDetails));
}
const cachedScriptletDetails = {
bcSecret,
code: contentScript.join('\n\n'),
filters: scriptletDetails.filters,
};
if ( request.nocache !== true ) {
this.scriptletCache.add(hostname, cachedScriptletDetails);
}
return cachedScriptletDetails;
}
injectNow(details) {
if ( typeof details.frameId !== 'number' ) { return; }
const hostname = hostnameFromURI(details.url);
const domain = domainFromHostname(hostname);
const scriptletDetails = this.retrieve({
tabId: details.tabId,
frameId: details.frameId,
url: details.url,
hostname,
domain,
entity: entityFromDomain(domain),
});
if ( scriptletDetails === undefined ) {
contentScriptRegisterer.unregister(hostname);
return;
}
if ( Boolean(scriptletDetails.code) === false ) {
return scriptletDetails;
}
const contentScript = [ scriptletDetails.code ];
if ( logger.enabled ) {
contentScript.unshift(
onScriptletMessageInjector.assemble(scriptletDetails)
);
}
if ( µb.hiddenSettings.debugScriptletInjector ) {
contentScript.unshift('debugger');
}
const code = contentScript.join('\n\n');
const isAlreadyInjected = contentScriptRegisterer.register(hostname, code);
if ( isAlreadyInjected !== true ) {
vAPI.tabs.executeScript(details.tabId, {
code,
frameId: details.frameId,
matchAboutBlank: true,
runAt: 'document_start',
});
}
return scriptletDetails;
}
toLogger(request, details) {
if ( details === undefined ) { return; }
if ( logger.enabled !== true ) { return; }
if ( typeof details.filters !== 'string' ) { return; }
const fctxt = µb.filteringContext
.duplicate()
.fromTabId(request.tabId)
.setRealm('extended')
.setType('scriptlet')
.setURL(request.url)
.setDocOriginFromURL(request.url);
for ( const raw of details.filters.split('\n') ) {
fctxt.setFilter({ source: 'extended', raw }).toLogger();
}
}
}
/******************************************************************************/
const scriptletFilteringEngine = new ScriptletFilteringEngineEx();
export default scriptletFilteringEngine;
/******************************************************************************/

View File

@@ -0,0 +1,365 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2015-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
*/
/* globals browser */
'use strict';
/******************************************************************************/
(( ) => {
// >>>>>>>> start of private namespace
/******************************************************************************/
if ( typeof vAPI !== 'object' ) { return; }
if ( vAPI.domWatcher instanceof Object === false ) { return; }
const reHasCSSCombinators = /[ >+~]/;
const simpleDeclarativeSet = new Set();
let simpleDeclarativeStr;
const complexDeclarativeSet = new Set();
let complexDeclarativeStr;
const proceduralDict = new Map();
const exceptionDict = new Map();
let exceptionStr;
const proceduralExceptionDict = new Map();
const nodesToProcess = new Set();
const loggedSelectors = new Set();
/******************************************************************************/
const rePseudoElements = /:(?::?after|:?before|:[a-z-]+)$/;
function hasSelector(selector, context = document) {
try {
return context.querySelector(selector) !== null;
}
catch(ex) {
}
return false;
}
function safeMatchSelector(selector, context) {
const safeSelector = rePseudoElements.test(selector)
? selector.replace(rePseudoElements, '')
: selector;
try {
return context.matches(safeSelector);
}
catch(ex) {
}
return false;
}
function safeQuerySelector(selector, context = document) {
const safeSelector = rePseudoElements.test(selector)
? selector.replace(rePseudoElements, '')
: selector;
try {
return context.querySelector(safeSelector);
}
catch(ex) {
}
return null;
}
function safeGroupSelectors(selectors) {
const arr = Array.isArray(selectors)
? selectors
: Array.from(selectors);
return arr.map(s => {
return rePseudoElements.test(s)
? s.replace(rePseudoElements, '')
: s;
}).join(',\n');
}
/******************************************************************************/
function processDeclarativeSimple(node, out) {
if ( simpleDeclarativeSet.size === 0 ) { return; }
if ( simpleDeclarativeStr === undefined ) {
simpleDeclarativeStr = safeGroupSelectors(simpleDeclarativeSet);
}
if (
(node === document || node.matches(simpleDeclarativeStr) === false) &&
(hasSelector(simpleDeclarativeStr, node) === false)
) {
return;
}
for ( const selector of simpleDeclarativeSet ) {
if (
(node === document || safeMatchSelector(selector, node) === false) &&
(safeQuerySelector(selector, node) === null)
) {
continue;
}
out.push(`##${selector}`);
simpleDeclarativeSet.delete(selector);
simpleDeclarativeStr = undefined;
loggedSelectors.add(selector);
}
}
/******************************************************************************/
function processDeclarativeComplex(out) {
if ( complexDeclarativeSet.size === 0 ) { return; }
if ( complexDeclarativeStr === undefined ) {
complexDeclarativeStr = safeGroupSelectors(complexDeclarativeSet);
}
if ( hasSelector(complexDeclarativeStr) === false ) { return; }
for ( const selector of complexDeclarativeSet ) {
if ( safeQuerySelector(selector) === null ) { continue; }
out.push(`##${selector}`);
complexDeclarativeSet.delete(selector);
complexDeclarativeStr = undefined;
loggedSelectors.add(selector);
}
}
/******************************************************************************/
function processProcedural(out) {
if ( proceduralDict.size === 0 ) { return; }
for ( const [ raw, pselector ] of proceduralDict ) {
if ( pselector.converted ) {
if ( safeQuerySelector(pselector.selector) === null ) { continue; }
} else if ( pselector.hit === false && pselector.exec().length === 0 ) {
continue;
}
out.push(`##${raw}`);
proceduralDict.delete(raw);
}
}
/******************************************************************************/
function processExceptions(out) {
if ( exceptionDict.size === 0 ) { return; }
if ( exceptionStr === undefined ) {
exceptionStr = safeGroupSelectors(exceptionDict.keys());
}
if ( hasSelector(exceptionStr) === false ) { return; }
for ( const [ selector, raw ] of exceptionDict ) {
if ( safeQuerySelector(selector) === null ) { continue; }
out.push(`#@#${raw}`);
exceptionDict.delete(selector);
exceptionStr = undefined;
loggedSelectors.add(raw);
}
}
/******************************************************************************/
function processProceduralExceptions(out) {
if ( proceduralExceptionDict.size === 0 ) { return; }
for ( const exception of proceduralExceptionDict.values() ) {
if ( exception.test() === false ) { continue; }
out.push(`#@#${exception.raw}`);
proceduralExceptionDict.delete(exception.raw);
}
}
/******************************************************************************/
const processTimer = new vAPI.SafeAnimationFrame(( ) => {
//console.time('dom logger/scanning for matches');
processTimer.clear();
if ( nodesToProcess.size === 0 ) { return; }
if ( nodesToProcess.size !== 1 && nodesToProcess.has(document) ) {
nodesToProcess.clear();
nodesToProcess.add(document);
}
const toLog = [];
if ( simpleDeclarativeSet.size !== 0 ) {
for ( const node of nodesToProcess ) {
processDeclarativeSimple(node, toLog);
}
}
processDeclarativeComplex(toLog);
processProcedural(toLog);
processExceptions(toLog);
processProceduralExceptions(toLog);
nodesToProcess.clear();
if ( toLog.length === 0 ) { return; }
const location = vAPI.effectiveSelf.location;
vAPI.messaging.send('scriptlets', {
what: 'logCosmeticFilteringData',
frameURL: location.href,
frameHostname: location.hostname,
matchedSelectors: toLog,
});
//console.timeEnd('dom logger/scanning for matches');
});
/******************************************************************************/
const attributeObserver = new MutationObserver(mutations => {
if ( nodesToProcess.has(document) ) { return; }
for ( const mutation of mutations ) {
const node = mutation.target;
if ( node.nodeType !== 1 ) { continue; }
nodesToProcess.add(node);
}
if ( nodesToProcess.size !== 0 ) {
processTimer.start(100);
}
});
/******************************************************************************/
const handlers = {
onFiltersetChanged: function(changes) {
//console.time('dom logger/filterset changed');
for ( const block of (changes.declarative || []) ) {
for ( const selector of block.split(',\n') ) {
if ( loggedSelectors.has(selector) ) { continue; }
if ( reHasCSSCombinators.test(selector) ) {
complexDeclarativeSet.add(selector);
complexDeclarativeStr = undefined;
} else {
simpleDeclarativeSet.add(selector);
simpleDeclarativeStr = undefined;
}
}
}
if (
Array.isArray(changes.procedural) &&
changes.procedural.length !== 0
) {
for ( const selector of changes.procedural ) {
proceduralDict.set(selector.raw, selector);
}
}
if ( Array.isArray(changes.exceptions) ) {
for ( const selector of changes.exceptions ) {
if ( loggedSelectors.has(selector) ) { continue; }
if ( selector.charCodeAt(0) !== 0x7B /* '{' */ ) {
exceptionDict.set(selector, selector);
continue;
}
const details = JSON.parse(selector);
if (
details.action !== undefined &&
details.tasks === undefined &&
details.action[0] === 'style'
) {
exceptionDict.set(details.selector, details.raw);
continue;
}
proceduralExceptionDict.set(
details.raw,
vAPI.domFilterer.createProceduralFilter(details)
);
}
exceptionStr = undefined;
}
nodesToProcess.clear();
nodesToProcess.add(document);
processTimer.start(1);
//console.timeEnd('dom logger/filterset changed');
},
onDOMCreated: function() {
if ( vAPI.domFilterer instanceof Object === false ) {
return shutdown();
}
handlers.onFiltersetChanged(vAPI.domFilterer.getAllSelectors());
vAPI.domFilterer.addListener(handlers);
attributeObserver.observe(document.body, {
attributes: true,
subtree: true
});
},
onDOMChanged: function(addedNodes) {
if ( nodesToProcess.has(document) ) { return; }
for ( const node of addedNodes ) {
if ( node.parentNode === null ) { continue; }
nodesToProcess.add(node);
}
if ( nodesToProcess.size !== 0 ) {
processTimer.start(100);
}
}
};
vAPI.domWatcher.addListener(handlers);
/******************************************************************************/
const broadcastHandler = msg => {
if ( msg.what === 'loggerDisabled' ) {
shutdown();
}
};
browser.runtime.onMessage.addListener(broadcastHandler);
/******************************************************************************/
function shutdown() {
browser.runtime.onMessage.removeListener(broadcastHandler);
processTimer.clear();
attributeObserver.disconnect();
if ( typeof vAPI !== 'object' ) { return; }
if ( vAPI.domFilterer instanceof Object ) {
vAPI.domFilterer.removeListener(handlers);
}
if ( vAPI.domWatcher instanceof Object ) {
vAPI.domWatcher.removeListener(handlers);
}
}
/******************************************************************************/
// <<<<<<<< end of private namespace
})();
/*******************************************************************************
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;

View File

@@ -0,0 +1,48 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2015-2018 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 ( typeof vAPI === 'object' && vAPI.domFilterer ) {
vAPI.domFilterer.toggle(false);
}
/*******************************************************************************
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;

View File

@@ -0,0 +1,48 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2015-2018 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 ( typeof vAPI === 'object' && vAPI.domFilterer ) {
vAPI.domFilterer.toggle(true);
}
/*******************************************************************************
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;

View File

@@ -0,0 +1,142 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2015-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';
/******************************************************************************/
(( ) => {
// >>>>>>>> start of private namespace
/******************************************************************************/
if ( typeof vAPI !== 'object' ) { return; }
if ( typeof vAPI.domFilterer !== 'object' ) { return; }
if ( vAPI.domFilterer === null ) { return; }
/******************************************************************************/
const rePseudoElements = /:(?::?after|:?before|:[a-z-]+)$/;
const hasSelector = selector => {
try {
return document.querySelector(selector) !== null;
}
catch(ex) {
}
return false;
};
const safeQuerySelector = selector => {
const safeSelector = rePseudoElements.test(selector)
? selector.replace(rePseudoElements, '')
: selector;
try {
return document.querySelector(safeSelector);
}
catch(ex) {
}
return null;
};
const safeGroupSelectors = selectors => {
const arr = Array.isArray(selectors)
? selectors
: Array.from(selectors);
return arr.map(s => {
return rePseudoElements.test(s)
? s.replace(rePseudoElements, '')
: s;
}).join(',\n');
};
const allSelectors = vAPI.domFilterer.getAllSelectors();
const matchedSelectors = [];
if ( Array.isArray(allSelectors.declarative) ) {
const declarativeSet = new Set();
for ( const block of allSelectors.declarative ) {
for ( const selector of block.split(',\n') ) {
declarativeSet.add(selector);
}
}
if ( hasSelector(safeGroupSelectors(declarativeSet)) ) {
for ( const selector of declarativeSet ) {
if ( safeQuerySelector(selector) === null ) { continue; }
matchedSelectors.push(`##${selector}`);
}
}
}
if (
Array.isArray(allSelectors.procedural) &&
allSelectors.procedural.length !== 0
) {
for ( const pselector of allSelectors.procedural ) {
if ( pselector.hit === false && pselector.exec().length === 0 ) { continue; }
matchedSelectors.push(`##${pselector.raw}`);
}
}
if ( Array.isArray(allSelectors.exceptions) ) {
const exceptionDict = new Map();
for ( const selector of allSelectors.exceptions ) {
if ( selector.charCodeAt(0) !== 0x7B /* '{' */ ) {
exceptionDict.set(selector, selector);
continue;
}
const details = JSON.parse(selector);
if (
details.action !== undefined &&
details.tasks === undefined &&
details.action[0] === 'style'
) {
exceptionDict.set(details.selector, details.raw);
continue;
}
const pselector = vAPI.domFilterer.createProceduralFilter(details);
if ( pselector.test() === false ) { continue; }
matchedSelectors.push(`#@#${pselector.raw}`);
}
if (
exceptionDict.size !== 0 &&
hasSelector(safeGroupSelectors(exceptionDict.keys()))
) {
for ( const [ selector, raw ] of exceptionDict ) {
if ( safeQuerySelector(selector) === null ) { continue; }
matchedSelectors.push(`#@#${raw}`);
}
}
}
if ( typeof self.uBO_scriptletsInjected === 'string' ) {
matchedSelectors.push(...self.uBO_scriptletsInjected.split('\n'));
}
if ( matchedSelectors.length === 0 ) { return; }
return matchedSelectors;
/******************************************************************************/
// <<<<<<<< end of private namespace
})();

View File

@@ -0,0 +1,920 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2015-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
*/
/******************************************************************************/
/******************************************************************************/
(async ( ) => {
/******************************************************************************/
if ( typeof vAPI !== 'object' ) { return; }
if ( vAPI === null ) { return; }
if ( vAPI.domFilterer instanceof Object === false ) { return; }
if ( vAPI.inspectorFrame ) { return; }
vAPI.inspectorFrame = true;
const inspectorUniqueId = vAPI.randomToken();
const nodeToIdMap = new WeakMap(); // No need to iterate
let blueNodes = [];
const roRedNodes = new Map(); // node => current cosmetic filter
const rwRedNodes = new Set(); // node => new cosmetic filter (toggle node)
const rwGreenNodes = new Set(); // node => new exception cosmetic filter (toggle filter)
//const roGreenNodes = new Map(); // node => current exception cosmetic filter (can't toggle)
const reHasCSSCombinators = /[ >+~]/;
/******************************************************************************/
const domLayout = (( ) => {
const skipTagNames = new Set([
'br', 'head', 'link', 'meta', 'script', 'style', 'title'
]);
const resourceAttrNames = new Map([
[ 'a', 'href' ],
[ 'iframe', 'src' ],
[ 'img', 'src' ],
[ 'object', 'data' ]
]);
let idGenerator = 1;
// This will be used to uniquely identify nodes across process.
const newNodeId = node => {
const nid = `n${(idGenerator++).toString(36)}`;
nodeToIdMap.set(node, nid);
return nid;
};
const selectorFromNode = node => {
const tag = node.localName;
let selector = CSS.escape(tag);
// Id
if ( typeof node.id === 'string' ) {
let str = node.id.trim();
if ( str !== '' ) {
selector += `#${CSS.escape(str)}`;
}
}
// Class
const cl = node.classList;
if ( cl ) {
for ( let i = 0; i < cl.length; i++ ) {
selector += `.${CSS.escape(cl[i])}`;
}
}
// Tag-specific attributes
const attr = resourceAttrNames.get(tag);
if ( attr !== undefined ) {
let str = node.getAttribute(attr) || '';
str = str.trim();
const pos = str.startsWith('data:') ? 5 : str.search(/[#?]/);
let sw = '';
if ( pos !== -1 ) {
str = str.slice(0, pos);
sw = '^';
}
if ( str !== '' ) {
selector += `[${attr}${sw}="${CSS.escape(str, true)}"]`;
}
}
return selector;
};
function DomRoot() {
this.nid = newNodeId(document.body);
this.lvl = 0;
this.sel = 'body';
this.cnt = 0;
this.filter = roRedNodes.get(document.body);
}
function DomNode(node, level) {
this.nid = newNodeId(node);
this.lvl = level;
this.sel = selectorFromNode(node);
this.cnt = 0;
this.filter = roRedNodes.get(node);
}
const domNodeFactory = (level, node) => {
const localName = node.localName;
if ( skipTagNames.has(localName) ) { return null; }
// skip uBlock's own nodes
if ( node === inspectorFrame ) { return null; }
if ( level === 0 && localName === 'body' ) {
return new DomRoot();
}
return new DomNode(node, level);
};
// Collect layout data
const getLayoutData = ( ) => {
const layout = [];
const stack = [];
let lvl = 0;
let node = document.documentElement;
if ( node === null ) { return layout; }
for (;;) {
const domNode = domNodeFactory(lvl, node);
if ( domNode !== null ) {
layout.push(domNode);
}
// children
if ( domNode !== null && node.firstElementChild !== null ) {
stack.push(node);
lvl += 1;
node = node.firstElementChild;
continue;
}
// sibling
if ( node instanceof Element ) {
if ( node.nextElementSibling === null ) {
do {
node = stack.pop();
if ( !node ) { break; }
lvl -= 1;
} while ( node.nextElementSibling === null );
if ( !node ) { break; }
}
node = node.nextElementSibling;
}
}
return layout;
};
// Descendant count for each node.
const patchLayoutData = layout => {
const stack = [];
let ptr;
let lvl = 0;
let i = layout.length;
while ( i-- ) {
const domNode = layout[i];
if ( domNode.lvl === lvl ) {
stack[ptr] += 1;
continue;
}
if ( domNode.lvl > lvl ) {
while ( lvl < domNode.lvl ) {
stack.push(0);
lvl += 1;
}
ptr = lvl - 1;
stack[ptr] += 1;
continue;
}
// domNode.lvl < lvl
const cnt = stack.pop();
domNode.cnt = cnt;
lvl -= 1;
ptr = lvl - 1;
stack[ptr] += cnt + 1;
}
return layout;
};
// Track and report mutations of the DOM
let mutationObserver = null;
let mutationTimer;
let addedNodelists = [];
let removedNodelist = [];
const previousElementSiblingId = node => {
let sibling = node;
for (;;) {
sibling = sibling.previousElementSibling;
if ( sibling === null ) { return null; }
if ( skipTagNames.has(sibling.localName) ) { continue; }
return nodeToIdMap.get(sibling);
}
};
const journalFromBranch = (root, newNodes, newNodeToIdMap) => {
let node = root.firstElementChild;
while ( node !== null ) {
const domNode = domNodeFactory(undefined, node);
if ( domNode !== null ) {
newNodeToIdMap.set(domNode.nid, domNode);
newNodes.push(node);
}
// down
if ( node.firstElementChild !== null ) {
node = node.firstElementChild;
continue;
}
// right
if ( node.nextElementSibling !== null ) {
node = node.nextElementSibling;
continue;
}
// up then right
for (;;) {
if ( node.parentElement === root ) { return; }
node = node.parentElement;
if ( node.nextElementSibling !== null ) {
node = node.nextElementSibling;
break;
}
}
}
};
const journalFromMutations = ( ) => {
mutationTimer = undefined;
// This is used to temporarily hold all added nodes, before resolving
// their node id and relative position.
const newNodes = [];
const journalEntries = [];
const newNodeToIdMap = new Map();
for ( const nodelist of addedNodelists ) {
for ( const node of nodelist ) {
if ( node.nodeType !== 1 ) { continue; }
if ( node.parentElement === null ) { continue; }
cosmeticFilterMapper.incremental(node);
const domNode = domNodeFactory(undefined, node);
if ( domNode !== null ) {
newNodeToIdMap.set(domNode.nid, domNode);
newNodes.push(node);
}
journalFromBranch(node, newNodes, newNodeToIdMap);
}
}
addedNodelists = [];
for ( const nodelist of removedNodelist ) {
for ( const node of nodelist ) {
if ( node.nodeType !== 1 ) { continue; }
const nid = nodeToIdMap.get(node);
if ( nid === undefined ) { continue; }
journalEntries.push({ what: -1, nid });
}
}
removedNodelist = [];
for ( const node of newNodes ) {
journalEntries.push({
what: 1,
nid: nodeToIdMap.get(node),
u: nodeToIdMap.get(node.parentElement),
l: previousElementSiblingId(node)
});
}
if ( journalEntries.length === 0 ) { return; }
contentInspectorChannel.toLogger({
what: 'domLayoutIncremental',
url: window.location.href,
hostname: window.location.hostname,
journal: journalEntries,
nodes: Array.from(newNodeToIdMap)
});
};
const onMutationObserved = mutationRecords => {
for ( const record of mutationRecords ) {
if ( record.addedNodes.length !== 0 ) {
addedNodelists.push(record.addedNodes);
}
if ( record.removedNodes.length !== 0 ) {
removedNodelist.push(record.removedNodes);
}
}
if ( mutationTimer === undefined ) {
mutationTimer = vAPI.setTimeout(journalFromMutations, 1000);
}
};
// API
const getLayout = ( ) => {
cosmeticFilterMapper.reset();
mutationObserver = new MutationObserver(onMutationObserved);
mutationObserver.observe(document.body, {
childList: true,
subtree: true
});
return {
what: 'domLayoutFull',
url: window.location.href,
hostname: window.location.hostname,
layout: patchLayoutData(getLayoutData())
};
};
const reset = ( ) => {
shutdown();
};
const shutdown = ( ) => {
if ( mutationTimer !== undefined ) {
clearTimeout(mutationTimer);
mutationTimer = undefined;
}
if ( mutationObserver !== null ) {
mutationObserver.disconnect();
mutationObserver = null;
}
addedNodelists = [];
removedNodelist = [];
};
return {
get: getLayout,
reset,
shutdown,
};
})();
/******************************************************************************/
/******************************************************************************/
const cosmeticFilterMapper = (( ) => {
const nodesFromStyleTag = rootNode => {
const filterMap = roRedNodes;
const details = vAPI.domFilterer.getAllSelectors();
// Declarative selectors.
for ( const block of (details.declarative || []) ) {
for ( const selector of block.split(',\n') ) {
let nodes;
if ( reHasCSSCombinators.test(selector) ) {
nodes = document.querySelectorAll(selector);
} else {
if (
filterMap.has(rootNode) === false &&
rootNode.matches(selector)
) {
filterMap.set(rootNode, selector);
}
nodes = rootNode.querySelectorAll(selector);
}
for ( const node of nodes ) {
if ( filterMap.has(node) ) { continue; }
filterMap.set(node, selector);
}
}
}
// Procedural selectors.
for ( const entry of (details.procedural || []) ) {
const nodes = entry.exec();
for ( const node of nodes ) {
// Upgrade declarative selector to procedural one
filterMap.set(node, entry.raw);
}
}
};
const incremental = rootNode => {
nodesFromStyleTag(rootNode);
};
const reset = ( ) => {
roRedNodes.clear();
if ( document.documentElement !== null ) {
incremental(document.documentElement);
}
};
const shutdown = ( ) => {
vAPI.domFilterer.toggle(true);
};
return {
incremental,
reset,
shutdown,
};
})();
/******************************************************************************/
const elementsFromSelector = function(selector, context) {
if ( !context ) {
context = document;
}
if ( selector.indexOf(':') !== -1 ) {
const out = elementsFromSpecialSelector(selector);
if ( out !== undefined ) { return out; }
}
// plain CSS selector
try {
return context.querySelectorAll(selector);
} catch (ex) {
}
return [];
};
const elementsFromSpecialSelector = function(selector) {
const out = [];
let matches = /^(.+?):has\((.+?)\)$/.exec(selector);
if ( matches !== null ) {
let nodes;
try {
nodes = document.querySelectorAll(matches[1]);
} catch(ex) {
nodes = [];
}
for ( const node of nodes ) {
if ( node.querySelector(matches[2]) === null ) { continue; }
out.push(node);
}
return out;
}
matches = /^:xpath\((.+?)\)$/.exec(selector);
if ( matches === null ) { return; }
const xpr = document.evaluate(
matches[1],
document,
null,
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
null
);
let i = xpr.snapshotLength;
while ( i-- ) {
out.push(xpr.snapshotItem(i));
}
return out;
};
/******************************************************************************/
const highlightElements = ( ) => {
const paths = [];
const path = [];
for ( const elem of rwRedNodes.keys() ) {
if ( elem === inspectorFrame ) { continue; }
if ( rwGreenNodes.has(elem) ) { continue; }
if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; }
const rect = elem.getBoundingClientRect();
const xl = rect.left;
const w = rect.width;
const yt = rect.top;
const h = rect.height;
const ws = w.toFixed(1);
const poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) +
'h' + ws +
'v' + h.toFixed(1) +
'h-' + ws +
'z';
path.push(poly);
}
paths.push(path.join('') || 'M0 0');
path.length = 0;
for ( const elem of rwGreenNodes ) {
if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; }
const rect = elem.getBoundingClientRect();
const xl = rect.left;
const w = rect.width;
const yt = rect.top;
const h = rect.height;
const ws = w.toFixed(1);
const poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) +
'h' + ws +
'v' + h.toFixed(1) +
'h-' + ws +
'z';
path.push(poly);
}
paths.push(path.join('') || 'M0 0');
path.length = 0;
for ( const elem of roRedNodes.keys() ) {
if ( elem === inspectorFrame ) { continue; }
if ( rwGreenNodes.has(elem) ) { continue; }
if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; }
const rect = elem.getBoundingClientRect();
const xl = rect.left;
const w = rect.width;
const yt = rect.top;
const h = rect.height;
const ws = w.toFixed(1);
const poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) +
'h' + ws +
'v' + h.toFixed(1) +
'h-' + ws +
'z';
path.push(poly);
}
paths.push(path.join('') || 'M0 0');
path.length = 0;
for ( const elem of blueNodes ) {
if ( elem === inspectorFrame ) { continue; }
if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; }
const rect = elem.getBoundingClientRect();
const xl = rect.left;
const w = rect.width;
const yt = rect.top;
const h = rect.height;
const ws = w.toFixed(1);
const poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) +
'h' + ws +
'v' + h.toFixed(1) +
'h-' + ws +
'z';
path.push(poly);
}
paths.push(path.join('') || 'M0 0');
contentInspectorChannel.toFrame({
what: 'svgPaths',
paths,
});
};
/******************************************************************************/
const onScrolled = (( ) => {
let timer;
return ( ) => {
if ( timer ) { return; }
timer = window.requestAnimationFrame(( ) => {
timer = undefined;
highlightElements();
});
};
})();
const onMouseOver = ( ) => {
if ( blueNodes.length === 0 ) { return; }
blueNodes = [];
highlightElements();
};
/******************************************************************************/
const selectNodes = (selector, nid) => {
const nodes = elementsFromSelector(selector);
if ( nid === '' ) { return nodes; }
for ( const node of nodes ) {
if ( nodeToIdMap.get(node) === nid ) {
return [ node ];
}
}
return [];
};
/******************************************************************************/
const nodesFromFilter = selector => {
const out = [];
for ( const entry of roRedNodes ) {
if ( entry[1] === selector ) {
out.push(entry[0]);
}
}
return out;
};
/******************************************************************************/
const toggleExceptions = (nodes, targetState) => {
for ( const node of nodes ) {
if ( targetState ) {
rwGreenNodes.add(node);
} else {
rwGreenNodes.delete(node);
}
}
};
const toggleFilter = (nodes, targetState) => {
for ( const node of nodes ) {
if ( targetState ) {
rwRedNodes.delete(node);
} else {
rwRedNodes.add(node);
}
}
};
const resetToggledNodes = ( ) => {
rwGreenNodes.clear();
rwRedNodes.clear();
};
/******************************************************************************/
const startInspector = ( ) => {
const onReady = ( ) => {
window.addEventListener('scroll', onScrolled, {
capture: true,
passive: true,
});
window.addEventListener('mouseover', onMouseOver, {
capture: true,
passive: true,
});
contentInspectorChannel.toLogger(domLayout.get());
vAPI.domFilterer.toggle(false, highlightElements);
};
if ( document.readyState === 'loading' ) {
document.addEventListener('DOMContentLoaded', onReady, { once: true });
} else {
onReady();
}
};
/******************************************************************************/
const shutdownInspector = ( ) => {
cosmeticFilterMapper.shutdown();
domLayout.shutdown();
window.removeEventListener('scroll', onScrolled, {
capture: true,
passive: true,
});
window.removeEventListener('mouseover', onMouseOver, {
capture: true,
passive: true,
});
contentInspectorChannel.shutdown();
if ( inspectorFrame ) {
inspectorFrame.remove();
inspectorFrame = null;
}
vAPI.userStylesheet.remove(inspectorCSS);
vAPI.userStylesheet.apply();
vAPI.inspectorFrame = false;
};
/******************************************************************************/
/******************************************************************************/
const onMessage = request => {
switch ( request.what ) {
case 'startInspector':
startInspector();
break;
case 'quitInspector':
shutdownInspector();
break;
case 'commitFilters':
highlightElements();
break;
case 'domLayout':
domLayout.get();
highlightElements();
break;
case 'highlightMode':
break;
case 'highlightOne':
blueNodes = selectNodes(request.selector, request.nid);
if ( blueNodes.length !== 0 ) {
blueNodes[0].scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest',
});
}
highlightElements();
break;
case 'resetToggledNodes':
resetToggledNodes();
highlightElements();
break;
case 'showCommitted':
blueNodes = [];
// TODO: show only the new filters and exceptions.
highlightElements();
break;
case 'showInteractive':
blueNodes = [];
highlightElements();
break;
case 'toggleFilter': {
const nodes = selectNodes(request.selector, request.nid);
if ( nodes.length !== 0 ) {
nodes[0].scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest',
});
}
toggleExceptions(nodesFromFilter(request.filter), request.target);
highlightElements();
break;
}
case 'toggleNodes': {
const nodes = selectNodes(request.selector, request.nid);
if ( nodes.length !== 0 ) {
nodes[0].scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest',
});
}
toggleFilter(nodes, request.target);
highlightElements();
break;
}
default:
break;
}
};
/*******************************************************************************
*
* Establish two-way communication with logger/inspector window and
* inspector frame
*
* */
const contentInspectorChannel = (( ) => {
let toLoggerPort;
let toFramePort;
const toLogger = msg => {
if ( toLoggerPort === undefined ) { return; }
try {
toLoggerPort.postMessage(msg);
} catch(_) {
shutdownInspector();
}
};
const onLoggerMessage = msg => {
onMessage(msg);
};
const onLoggerDisconnect = ( ) => {
shutdownInspector();
};
const onLoggerConnect = port => {
browser.runtime.onConnect.removeListener(onLoggerConnect);
toLoggerPort = port;
port.onMessage.addListener(onLoggerMessage);
port.onDisconnect.addListener(onLoggerDisconnect);
};
const toFrame = msg => {
if ( toFramePort === undefined ) { return; }
toFramePort.postMessage(msg);
};
const shutdown = ( ) => {
if ( toFramePort !== undefined ) {
toFrame({ what: 'quitInspector' });
toFramePort.onmessage = null;
toFramePort.close();
toFramePort = undefined;
}
if ( toLoggerPort !== undefined ) {
toLoggerPort.onMessage.removeListener(onLoggerMessage);
toLoggerPort.onDisconnect.removeListener(onLoggerDisconnect);
toLoggerPort.disconnect();
toLoggerPort = undefined;
}
browser.runtime.onConnect.removeListener(onLoggerConnect);
};
const start = async ( ) => {
browser.runtime.onConnect.addListener(onLoggerConnect);
const inspectorArgs = await vAPI.messaging.send('domInspectorContent', {
what: 'getInspectorArgs',
});
if ( typeof inspectorArgs !== 'object' ) { return; }
if ( inspectorArgs === null ) { return; }
return new Promise(resolve => {
const iframe = document.createElement('iframe');
iframe.setAttribute(inspectorUniqueId, '');
document.documentElement.append(iframe);
iframe.addEventListener('load', ( ) => {
iframe.setAttribute(`${inspectorUniqueId}-loaded`, '');
const channel = new MessageChannel();
toFramePort = channel.port1;
toFramePort.onmessage = ev => {
const msg = ev.data || {};
if ( msg.what !== 'startInspector' ) { return; }
};
iframe.contentWindow.postMessage(
{ what: 'startInspector' },
inspectorArgs.inspectorURL,
[ channel.port2 ]
);
resolve(iframe);
}, { once: true });
iframe.contentWindow.location = inspectorArgs.inspectorURL;
});
};
return { start, toLogger, toFrame, shutdown };
})();
// Install DOM inspector widget
const inspectorCSSStyle = [
'background: transparent',
'border: 0',
'border-radius: 0',
'box-shadow: none',
'color-scheme: light dark',
'display: block',
'filter: none',
'height: 100%',
'left: 0',
'margin: 0',
'max-height: none',
'max-width: none',
'min-height: unset',
'min-width: unset',
'opacity: 1',
'outline: 0',
'padding: 0',
'pointer-events: none',
'position: fixed',
'top: 0',
'transform: none',
'visibility: hidden',
'width: 100%',
'z-index: 2147483647',
''
].join(' !important;\n');
const inspectorCSS = `
:root > [${inspectorUniqueId}] {
${inspectorCSSStyle}
}
:root > [${inspectorUniqueId}-loaded] {
visibility: visible !important;
}
`;
vAPI.userStylesheet.add(inspectorCSS);
vAPI.userStylesheet.apply();
let inspectorFrame = await contentInspectorChannel.start();
if ( inspectorFrame instanceof HTMLIFrameElement === false ) {
return shutdownInspector();
}
startInspector();
/******************************************************************************/
})();
/*******************************************************************************
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;

View File

@@ -0,0 +1,72 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2015-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';
/******************************************************************************/
// https://github.com/uBlockOrigin/uBlock-issues/issues/756
// Keep in mind CPU usage with large DOM and/or filterset.
(( ) => {
if ( typeof vAPI !== 'object' ) { return; }
const t0 = Date.now();
if ( vAPI.domSurveyElements instanceof Object === false ) {
vAPI.domSurveyElements = {
busy: false,
hiddenElementCount: Number.NaN,
surveyTime: t0,
};
}
const surveyResults = vAPI.domSurveyElements;
if ( surveyResults.busy ) { return; }
surveyResults.busy = true;
if ( surveyResults.surveyTime < vAPI.domMutationTime ) {
surveyResults.hiddenElementCount = Number.NaN;
}
surveyResults.surveyTime = t0;
if ( isNaN(surveyResults.hiddenElementCount) ) {
surveyResults.hiddenElementCount = (( ) => {
if ( vAPI.domFilterer instanceof Object === false ) { return 0; }
const details = vAPI.domFilterer.getAllSelectors(0b11);
if (
Array.isArray(details.declarative) === false ||
details.declarative.length === 0
) {
return 0;
}
return document.querySelectorAll(
details.declarative.join(',\n')
).length;
})();
}
surveyResults.busy = false;
// IMPORTANT: This is returned to the injector, so this MUST be
// the last statement.
return surveyResults.hiddenElementCount;
})();

View File

@@ -0,0 +1,126 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2015-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';
/******************************************************************************/
// Scriptlets to count the number of script tags in a document.
(( ) => {
if ( typeof vAPI !== 'object' ) { return; }
const t0 = Date.now();
if ( vAPI.domSurveyScripts instanceof Object === false ) {
vAPI.domSurveyScripts = {
busy: false,
scriptCount: -1,
surveyTime: t0,
};
}
const surveyResults = vAPI.domSurveyScripts;
if ( surveyResults.busy ) { return; }
surveyResults.busy = true;
if ( surveyResults.surveyTime < vAPI.domMutationTime ) {
surveyResults.scriptCount = -1;
}
surveyResults.surveyTime = t0;
if ( surveyResults.scriptCount === -1 ) {
const reInlineScript = /^(data:|blob:|$)/;
let inlineScriptCount = 0;
let scriptCount = 0;
for ( const script of document.scripts ) {
if ( reInlineScript.test(script.src) ) {
inlineScriptCount = 1;
continue;
}
scriptCount += 1;
if ( scriptCount === 99 ) { break; }
}
scriptCount += inlineScriptCount;
if ( scriptCount !== 0 ) {
surveyResults.scriptCount = scriptCount;
}
}
// https://github.com/uBlockOrigin/uBlock-issues/issues/756
// Keep trying to find inline script-like instances but only if we
// have the time-budget to do so.
if ( surveyResults.scriptCount === -1 ) {
if ( document.querySelector('a[href^="javascript:"]') !== null ) {
surveyResults.scriptCount = 1;
}
}
// https://github.com/uBlockOrigin/uBlock-issues/issues/1756
// Mind that there might be no body element.
if ( surveyResults.scriptCount === -1 && document.body !== null ) {
surveyResults.scriptCount = 0;
const onHandlers = new Set([
'onabort', 'onblur', 'oncancel', 'oncanplay',
'oncanplaythrough', 'onchange', 'onclick', 'onclose',
'oncontextmenu', 'oncuechange', 'ondblclick', 'ondrag',
'ondragend', 'ondragenter', 'ondragexit', 'ondragleave',
'ondragover', 'ondragstart', 'ondrop', 'ondurationchange',
'onemptied', 'onended', 'onerror', 'onfocus',
'oninput', 'oninvalid', 'onkeydown', 'onkeypress',
'onkeyup', 'onload', 'onloadeddata', 'onloadedmetadata',
'onloadstart', 'onmousedown', 'onmouseenter', 'onmouseleave',
'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup',
'onwheel', 'onpause', 'onplay', 'onplaying',
'onprogress', 'onratechange', 'onreset', 'onresize',
'onscroll', 'onseeked', 'onseeking', 'onselect',
'onshow', 'onstalled', 'onsubmit', 'onsuspend',
'ontimeupdate', 'ontoggle', 'onvolumechange', 'onwaiting',
'onafterprint', 'onbeforeprint', 'onbeforeunload', 'onhashchange',
'onlanguagechange', 'onmessage', 'onoffline', 'ononline',
'onpagehide', 'onpageshow', 'onrejectionhandled', 'onpopstate',
'onstorage', 'onunhandledrejection', 'onunload',
'oncopy', 'oncut', 'onpaste'
]);
const nodeIter = document.createNodeIterator(
document.body,
NodeFilter.SHOW_ELEMENT
);
for (;;) {
const node = nodeIter.nextNode();
if ( node === null ) { break; }
if ( node.hasAttributes() === false ) { continue; }
for ( const attr of node.getAttributeNames() ) {
if ( onHandlers.has(attr) === false ) { continue; }
surveyResults.scriptCount = 1;
break;
}
}
}
surveyResults.busy = false;
// IMPORTANT: This is returned to the injector, so this MUST be
// the last statement.
if ( surveyResults.scriptCount !== -1 ) {
return surveyResults.scriptCount;
}
})();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,67 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2020-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 ( typeof vAPI !== 'object' ) { return; }
if ( vAPI.dynamicReloadToken === undefined ) {
vAPI.dynamicReloadToken = vAPI.randomToken();
}
for ( const sheet of Array.from(document.styleSheets) ) {
let loaded = false;
try {
loaded = sheet.rules.length !== 0;
} catch(ex) {
}
if ( loaded ) { continue; }
const link = sheet.ownerNode || null;
if ( link === null || link.localName !== 'link' ) { continue; }
if ( link.hasAttribute(vAPI.dynamicReloadToken) ) { continue; }
const clone = link.cloneNode(true);
clone.setAttribute(vAPI.dynamicReloadToken, '');
link.replaceWith(clone);
}
})();
/*******************************************************************************
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;

View File

@@ -0,0 +1,62 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2015-2018 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 (
typeof vAPI !== 'object' ||
vAPI.loadAllLargeMedia instanceof Function === false
) {
return;
}
vAPI.loadAllLargeMedia();
vAPI.loadAllLargeMedia = undefined;
/******************************************************************************/
})();
/*******************************************************************************
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;

View File

@@ -0,0 +1,312 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2015-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
*/
(( ) => {
/******************************************************************************/
// This can happen
if ( typeof vAPI !== 'object' || vAPI.loadAllLargeMedia instanceof Function ) {
return;
}
const largeMediaElementAttribute = 'data-' + vAPI.sessionId;
const largeMediaElementSelector =
':root audio[' + largeMediaElementAttribute + '],\n' +
':root img[' + largeMediaElementAttribute + '],\n' +
':root picture[' + largeMediaElementAttribute + '],\n' +
':root video[' + largeMediaElementAttribute + ']';
const isMediaElement = elem =>
(/^(?:audio|img|picture|video)$/.test(elem.localName));
const isPlayableMediaElement = elem =>
(/^(?:audio|video)$/.test(elem.localName));
/******************************************************************************/
const mediaNotLoaded = function(elem) {
switch ( elem.localName ) {
case 'audio':
case 'video':
return elem.readyState === 0 || elem.error !== null;
case 'img': {
if ( elem.naturalWidth !== 0 || elem.naturalHeight !== 0 ) {
break;
}
const style = window.getComputedStyle(elem);
// For some reason, style can be null with Pale Moon.
return style !== null ?
style.getPropertyValue('display') !== 'none' :
elem.offsetHeight !== 0 && elem.offsetWidth !== 0;
}
default:
break;
}
return false;
};
/******************************************************************************/
// For all media resources which have failed to load, trigger a reload.
// <audio> and <video> elements.
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement
const surveyMissingMediaElements = function() {
let largeMediaElementCount = 0;
for ( const elem of document.querySelectorAll('audio,img,video') ) {
if ( mediaNotLoaded(elem) === false ) { continue; }
elem.setAttribute(largeMediaElementAttribute, '');
largeMediaElementCount += 1;
switch ( elem.localName ) {
case 'img': {
const picture = elem.closest('picture');
if ( picture !== null ) {
picture.setAttribute(largeMediaElementAttribute, '');
}
} break;
default:
break;
}
}
return largeMediaElementCount;
};
if ( surveyMissingMediaElements() ) {
// Insert CSS to highlight blocked media elements.
if ( vAPI.largeMediaElementStyleSheet === undefined ) {
vAPI.largeMediaElementStyleSheet = [
largeMediaElementSelector + ' {',
'border: 2px dotted red !important;',
'box-sizing: border-box !important;',
'cursor: zoom-in !important;',
'display: inline-block;',
'filter: none !important;',
'font-size: 1rem !important;',
'min-height: 1em !important;',
'min-width: 1em !important;',
'opacity: 1 !important;',
'outline: none !important;',
'transform: none !important;',
'visibility: visible !important;',
'z-index: 2147483647',
'}',
].join('\n');
vAPI.userStylesheet.add(vAPI.largeMediaElementStyleSheet);
vAPI.userStylesheet.apply();
}
}
/******************************************************************************/
const loadMedia = async function(elem) {
const src = elem.getAttribute('src') || '';
if ( src === '' ) { return; }
elem.removeAttribute('src');
await vAPI.messaging.send('scriptlets', {
what: 'temporarilyAllowLargeMediaElement',
});
elem.setAttribute('src', src);
elem.load();
};
/******************************************************************************/
const loadImage = async function(elem) {
const src = elem.getAttribute('src') || '';
const srcset = src === '' && elem.getAttribute('srcset') || '';
if ( src === '' && srcset === '' ) { return; }
if ( src !== '' ) {
elem.removeAttribute('src');
}
if ( srcset !== '' ) {
elem.removeAttribute('srcset');
}
await vAPI.messaging.send('scriptlets', {
what: 'temporarilyAllowLargeMediaElement',
});
if ( src !== '' ) {
elem.setAttribute('src', src);
} else if ( srcset !== '' ) {
elem.setAttribute('srcset', srcset);
}
};
/******************************************************************************/
const loadMany = function(elems) {
for ( const elem of elems ) {
switch ( elem.localName ) {
case 'audio':
case 'video':
loadMedia(elem);
break;
case 'img':
loadImage(elem);
break;
default:
break;
}
}
};
/******************************************************************************/
const onMouseClick = function(ev) {
if ( ev.button !== 0 || ev.isTrusted === false ) { return; }
const toLoad = [];
const elems = document.elementsFromPoint instanceof Function
? document.elementsFromPoint(ev.clientX, ev.clientY)
: [ ev.target ];
for ( const elem of elems ) {
if ( elem.matches(largeMediaElementSelector) === false ) { continue; }
elem.removeAttribute(largeMediaElementAttribute);
if ( mediaNotLoaded(elem) ) {
toLoad.push(elem);
}
}
if ( toLoad.length === 0 ) { return; }
loadMany(toLoad);
ev.preventDefault();
ev.stopPropagation();
};
document.addEventListener('click', onMouseClick, true);
/******************************************************************************/
const onLoadedData = function(ev) {
const media = ev.target;
if ( media.localName !== 'audio' && media.localName !== 'video' ) {
return;
}
const src = media.src;
if ( typeof src === 'string' && src.startsWith('blob:') === false ) {
return;
}
media.autoplay = false;
media.pause();
};
// https://www.reddit.com/r/uBlockOrigin/comments/mxgpmc/
// Support cases where the media source is not yet set.
for ( const media of document.querySelectorAll('audio,video') ) {
const src = media.src;
if (
(typeof src === 'string') &&
(src === '' || src.startsWith('blob:'))
) {
media.autoplay = false;
media.pause();
}
}
document.addEventListener('loadeddata', onLoadedData);
/******************************************************************************/
const onLoad = function(ev) {
const elem = ev.target;
if ( isMediaElement(elem) === false ) { return; }
elem.removeAttribute(largeMediaElementAttribute);
};
document.addEventListener('load', onLoad, true);
/******************************************************************************/
const onLoadError = function(ev) {
const elem = ev.target;
if ( isMediaElement(elem) === false ) { return; }
if ( mediaNotLoaded(elem) ) {
elem.setAttribute(largeMediaElementAttribute, '');
}
};
document.addEventListener('error', onLoadError, true);
/******************************************************************************/
const autoPausedMedia = new WeakMap();
for ( const elem of document.querySelectorAll('audio,video') ) {
elem.setAttribute('autoplay', 'false');
}
const preventAutoplay = function(ev) {
const elem = ev.target;
if ( isPlayableMediaElement(elem) === false ) { return; }
const currentSrc = elem.getAttribute('src') || '';
const pausedSrc = autoPausedMedia.get(elem);
if ( pausedSrc === currentSrc ) { return; }
autoPausedMedia.set(elem, currentSrc);
elem.setAttribute('autoplay', 'false');
elem.pause();
};
document.addEventListener('timeupdate', preventAutoplay, true);
/******************************************************************************/
vAPI.loadAllLargeMedia = function() {
document.removeEventListener('click', onMouseClick, true);
document.removeEventListener('loadeddata', onLoadedData, true);
document.removeEventListener('load', onLoad, true);
document.removeEventListener('error', onLoadError, true);
const toLoad = [];
for ( const elem of document.querySelectorAll(largeMediaElementSelector) ) {
elem.removeAttribute(largeMediaElementAttribute);
if ( mediaNotLoaded(elem) ) {
toLoad.push(elem);
}
}
loadMany(toLoad);
};
/******************************************************************************/
})();
/*******************************************************************************
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;

View File

@@ -0,0 +1,89 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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
*/
// Code below has been imported from uMatrix and modified to fit uBO:
// https://github.com/gorhill/uMatrix/blob/3f8794dd899a05e066c24066c6c0a2515d5c60d2/src/js/contentscript.js#L464-L531
'use strict';
/******************************************************************************/
// https://github.com/gorhill/uMatrix/issues/232
// Force `display` property, Firefox is still affected by the issue.
(( ) => {
const noscripts = document.querySelectorAll('noscript');
if ( noscripts.length === 0 ) { return; }
const reMetaContent = /^\s*(\d+)\s*;\s*url=(?:"([^"]+)"|'([^']+)'|(.+))/i;
const reSafeURL = /^https?:\/\//;
let redirectTimer;
const autoRefresh = function(root) {
const meta = root.querySelector('meta[http-equiv="refresh"][content]');
if ( meta === null ) { return; }
const match = reMetaContent.exec(meta.getAttribute('content'));
if ( match === null ) { return; }
const refreshURL = (match[2] || match[3] || match[4] || '').trim();
let url;
try {
url = new URL(refreshURL, document.baseURI);
} catch(ex) {
return;
}
if ( reSafeURL.test(url.href) === false ) { return; }
redirectTimer = setTimeout(( ) => {
location.assign(url.href);
},
parseInt(match[1], 10) * 1000 + 1
);
meta.parentNode.removeChild(meta);
};
const morphNoscript = function(from) {
if ( /^application\/(?:xhtml\+)?xml/.test(document.contentType) ) {
const to = document.createElement('span');
while ( from.firstChild !== null ) {
to.appendChild(from.firstChild);
}
return to;
}
const parser = new DOMParser();
const doc = parser.parseFromString(
'<span>' + from.textContent + '</span>',
'text/html'
);
return document.adoptNode(doc.querySelector('span'));
};
for ( const noscript of noscripts ) {
const parent = noscript.parentNode;
if ( parent === null ) { continue; }
const span = morphNoscript(noscript);
span.style.setProperty('display', 'inline', 'important');
if ( redirectTimer === undefined ) {
autoRefresh(span);
}
parent.replaceChild(span, noscript);
}
})();
/******************************************************************************/

View File

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

View File

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

View File

@@ -0,0 +1,38 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2018-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
*/
// If content scripts are already injected, we need to respond with `false`,
// to "should inject content scripts?"
//
// https://github.com/uBlockOrigin/uBlock-issues/issues/403
// If the content script was not bootstrapped, give it another try.
(( ) => {
try {
const status = vAPI.uBO !== true;
if ( status === false && vAPI.bootstrap ) {
self.requestIdleCallback(( ) => vAPI?.bootstrap?.());
}
return status;
} catch(ex) {
}
return true;
})();

View File

@@ -0,0 +1,113 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2015-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
*/
/* global HTMLDocument */
'use strict';
/******************************************************************************/
// Injected into specific web pages, those which have been pre-selected
// because they are known to contains `abp:subscribe` links.
/******************************************************************************/
(( ) => {
// >>>>> start of local scope
/******************************************************************************/
// https://github.com/chrisaljoudi/uBlock/issues/464
if ( document instanceof HTMLDocument === false ) { return; }
// Maybe uBO has gone away meanwhile.
if ( typeof vAPI !== 'object' || vAPI === null ) { return; }
const onMaybeSubscriptionLinkClicked = function(target) {
if ( vAPI instanceof Object === false ) {
document.removeEventListener('click', onMaybeSubscriptionLinkClicked);
return;
}
try {
// https://github.com/uBlockOrigin/uBlock-issues/issues/763#issuecomment-691696716
// Remove replacement patch if/when filterlists.com fixes encoded '&'.
const subscribeURL = new URL(
target.href.replace('&amp;title=', '&title=')
);
if (
/^(abp|ubo):$/.test(subscribeURL.protocol) === false &&
subscribeURL.hostname !== 'subscribe.adblockplus.org'
) {
return;
}
const location = subscribeURL.searchParams.get('location') || '';
const title = subscribeURL.searchParams.get('title') || '';
if ( location === '' || title === '' ) { return true; }
// https://github.com/uBlockOrigin/uBlock-issues/issues/1797
if ( /^(file|https?):\/\//.test(location) === false ) { return true; }
vAPI.messaging.send('scriptlets', {
what: 'subscribeTo',
location,
title,
});
return true;
} catch (_) {
}
};
// https://github.com/easylist/EasyListHebrew/issues/89
// Ensure trusted events only.
document.addEventListener('click', ev => {
if ( ev.button !== 0 || ev.isTrusted === false ) { return; }
const target = ev.target.closest('a');
if ( target instanceof HTMLAnchorElement === false ) { return; }
if ( onMaybeSubscriptionLinkClicked(target) === true ) {
ev.stopPropagation();
ev.preventDefault();
}
});
/******************************************************************************/
// <<<<< end of local scope
})();
/*******************************************************************************
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;

View File

@@ -0,0 +1,118 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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
*/
/* global HTMLDocument */
'use strict';
/******************************************************************************/
// Injected into specific webpages, those which have been pre-selected
// because they are known to contain:
// https://ublockorigin.github.io/uAssets/update-lists?listkeys=[...]
/******************************************************************************/
(( ) => {
// >>>>> start of local scope
/******************************************************************************/
if ( document instanceof HTMLDocument === false ) { return; }
// Maybe uBO has gone away meanwhile.
if ( typeof vAPI !== 'object' || vAPI === null ) { return; }
function updateStockLists(target) {
if ( vAPI instanceof Object === false ) {
document.removeEventListener('click', updateStockLists);
return;
}
try {
const updateURL = new URL(target.href);
if ( updateURL.hostname !== 'ublockorigin.github.io') { return; }
if ( updateURL.pathname !== '/uAssets/update-lists.html') { return; }
const listkeys = updateURL.searchParams.get('listkeys') || '';
if ( listkeys === '' ) { return; }
let auto = true;
const manual = updateURL.searchParams.get('manual');
if ( manual === '1' ) {
auto = false;
} else if ( /^\d{6}$/.test(`${manual}`) ) {
const year = parseInt(manual.slice(0,2)) || 0;
const month = parseInt(manual.slice(2,4)) || 0;
const day = parseInt(manual.slice(4,6)) || 0;
if ( year !== 0 && month !== 0 && day !== 0 ) {
const date = new Date();
date.setUTCFullYear(2000 + year, month - 1, day);
date.setUTCHours(0);
const then = date.getTime() / 1000 / 3600;
const now = Date.now() / 1000 / 3600;
auto = then < (now - 48) || then > (now + 48);
}
}
vAPI.messaging.send('scriptlets', {
what: 'updateLists',
listkeys,
auto,
});
return true;
} catch (_) {
}
}
// https://github.com/easylist/EasyListHebrew/issues/89
// Ensure trusted events only.
document.addEventListener('click', ev => {
if ( ev.button !== 0 || ev.isTrusted === false ) { return; }
const target = ev.target.closest('a');
if ( target instanceof HTMLAnchorElement === false ) { return; }
if ( updateStockLists(target) === true ) {
ev.stopPropagation();
ev.preventDefault();
}
});
/******************************************************************************/
// <<<<< end of local scope
})();
/*******************************************************************************
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;

View File

@@ -0,0 +1,328 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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';
import { i18n$ } from './i18n.js';
import { dom, qs$, qsa$ } from './dom.js';
import { setAccentColor, setTheme } from './theme.js';
/******************************************************************************/
function handleImportFilePicker() {
const file = this.files[0];
if ( file === undefined || file.name === '' ) { return; }
const reportError = ( ) => {
window.alert(i18n$('aboutRestoreDataError'));
};
const expectedFileTypes = [
'text/plain',
'application/json',
];
if ( expectedFileTypes.includes(file.type) === false ) {
return reportError();
}
const filename = file.name;
const fr = new FileReader();
fr.onload = function() {
let userData;
try {
userData = JSON.parse(this.result);
if ( typeof userData !== 'object' ) {
throw 'Invalid';
}
if ( typeof userData.userSettings !== 'object' ) {
throw 'Invalid';
}
if (
Array.isArray(userData.whitelist) === false &&
typeof userData.netWhitelist !== 'string'
) {
throw 'Invalid';
}
if (
typeof userData.filterLists !== 'object' &&
Array.isArray(userData.selectedFilterLists) === false
) {
throw 'Invalid';
}
}
catch (e) {
userData = undefined;
}
if ( userData === undefined ) {
return reportError();
}
const time = new Date(userData.timeStamp);
const msg = i18n$('aboutRestoreDataConfirm')
.replace('{{time}}', time.toLocaleString());
const proceed = window.confirm(msg);
if ( proceed !== true ) { return; }
vAPI.messaging.send('dashboard', {
what: 'restoreUserData',
userData,
file: filename,
});
};
fr.readAsText(file);
}
/******************************************************************************/
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();
}
/******************************************************************************/
async function exportToFile() {
const response = await vAPI.messaging.send('dashboard', {
what: 'backupUserData',
});
if (
response instanceof Object === false ||
response.userData instanceof Object === false
) {
return;
}
vAPI.download({
'url': 'data:text/plain;charset=utf-8,' +
encodeURIComponent(JSON.stringify(response.userData, null, ' ')),
'filename': response.localData.lastBackupFile
});
onLocalDataReceived(response.localData);
}
/******************************************************************************/
function onLocalDataReceived(details) {
let v, unit;
if ( typeof details.storageUsed === 'number' ) {
v = details.storageUsed;
if ( v < 1e3 ) {
unit = 'genericBytes';
} else if ( v < 1e6 ) {
v /= 1e3;
unit = 'KB';
} else if ( v < 1e9 ) {
v /= 1e6;
unit = 'MB';
} else {
v /= 1e9;
unit = 'GB';
}
} else {
v = '?';
unit = '';
}
dom.text(
'#storageUsed',
i18n$('storageUsed')
.replace('{{value}}', v.toLocaleString(undefined, { maximumSignificantDigits: 3 }))
.replace('{{unit}}', unit && i18n$(unit) || '')
);
const timeOptions = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
timeZoneName: 'short'
};
const lastBackupFile = details.lastBackupFile || '';
if ( lastBackupFile !== '' ) {
const dt = new Date(details.lastBackupTime);
const text = i18n$('settingsLastBackupPrompt');
const node = qs$('#settingsLastBackupPrompt');
node.textContent = text + '\xA0' + dt.toLocaleString('fullwide', timeOptions);
node.style.display = '';
}
const lastRestoreFile = details.lastRestoreFile || '';
if ( lastRestoreFile !== '' ) {
const dt = new Date(details.lastRestoreTime);
const text = i18n$('settingsLastRestorePrompt');
const node = qs$('#settingsLastRestorePrompt');
node.textContent = text + '\xA0' + dt.toLocaleString('fullwide', timeOptions);
node.style.display = '';
}
if ( details.cloudStorageSupported === false ) {
dom.attr('[data-setting-name="cloudStorageEnabled"]', 'disabled', '');
}
if ( details.privacySettingsSupported === false ) {
dom.attr('[data-setting-name="prefetchingDisabled"]', 'disabled', '');
dom.attr('[data-setting-name="hyperlinkAuditingDisabled"]', 'disabled', '');
dom.attr('[data-setting-name="webrtcIPAddressHidden"]', 'disabled', '');
}
}
/******************************************************************************/
function resetUserData() {
const msg = i18n$('aboutResetDataConfirm');
const proceed = window.confirm(msg);
if ( proceed !== true ) { return; }
vAPI.messaging.send('dashboard', {
what: 'resetUserData',
});
}
/******************************************************************************/
function synchronizeDOM() {
dom.cl.toggle(
dom.body,
'advancedUser',
qs$('[data-setting-name="advancedUserEnabled"]').checked === true
);
}
/******************************************************************************/
function changeUserSettings(name, value) {
vAPI.messaging.send('dashboard', {
what: 'userSettings',
name,
value,
});
// Maybe reflect some changes immediately
switch ( name ) {
case 'uiTheme':
setTheme(value, true);
break;
case 'uiAccentCustom':
case 'uiAccentCustom0':
setAccentColor(
qs$('[data-setting-name="uiAccentCustom"]').checked,
qs$('[data-setting-name="uiAccentCustom0"]').value,
true
);
break;
default:
break;
}
}
/******************************************************************************/
function onValueChanged(ev) {
const input = ev.target;
const name = dom.attr(input, 'data-setting-name');
let value = input.value;
// Maybe sanitize value
switch ( name ) {
case 'largeMediaSize':
value = Math.min(Math.max(Math.floor(parseInt(value, 10) || 0), 0), 1000000);
break;
default:
break;
}
if ( value !== input.value ) {
input.value = value;
}
changeUserSettings(name, value);
}
/******************************************************************************/
// TODO: use data-* to declare simple settings
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 ) {
dom.attr(checkbox.closest('.checkbox'), 'disabled', '');
dom.attr(checkbox, 'disabled', '');
continue;
}
checkbox.checked = details[name] === true;
dom.on(checkbox, 'change', onchange);
}
if ( details.canLeakLocalIPAddresses === true ) {
qs$('[data-setting-name="webrtcIPAddressHidden"]')
.closest('div.li')
.style.display = '';
}
qsa$('[data-setting-type="value"]').forEach(function(elem) {
elem.value = details[dom.attr(elem, 'data-setting-name')];
dom.on(elem, 'change', onValueChanged);
});
dom.on('#export', 'click', ( ) => { exportToFile(); });
dom.on('#import', 'click', startImportFilePicker);
dom.on('#reset', 'click', resetUserData);
dom.on('#restoreFilePicker', 'change', handleImportFilePicker);
synchronizeDOM();
}
/******************************************************************************/
self.wikilink = 'https://github.com/gorhill/uBlock/wiki/Dashboard:-Settings';
self.hasUnsavedData = function() {
return false;
};
/******************************************************************************/
vAPI.messaging.send('dashboard', { what: 'userSettings' }).then(result => {
onUserSettingsReceived(result);
});
vAPI.messaging.send('dashboard', { what: 'getLocalData' }).then(result => {
onLocalDataReceived(result);
});
// https://github.com/uBlockOrigin/uBlock-issues/issues/591
dom.on(
'[data-i18n-title="settingsAdvancedUserSettings"]',
'click',
self.uBlockDashboard.openOrSelectPage
);
/******************************************************************************/

View File

@@ -0,0 +1,559 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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
*/
/* globals browser */
'use strict';
/******************************************************************************/
import './vapi-common.js';
import './vapi-background.js';
import './vapi-background-ext.js';
/******************************************************************************/
// The following modules are loaded here until their content is better organized
import './commands.js';
import './messaging.js';
import './storage.js';
import './tab.js';
import './ublock.js';
import './utils.js';
import io from './assets.js';
import µb from './background.js';
import { filteringBehaviorChanged } from './broadcast.js';
import cacheStorage from './cachestorage.js';
import { ubolog } from './console.js';
import contextMenu from './contextmenu.js';
import { redirectEngine } from './redirect-engine.js';
import staticFilteringReverseLookup from './reverselookup.js';
import staticExtFilteringEngine from './static-ext-filtering.js';
import staticNetFilteringEngine from './static-net-filtering.js';
import webRequest from './traffic.js';
import {
permanentFirewall,
sessionFirewall,
permanentSwitches,
sessionSwitches,
permanentURLFiltering,
sessionURLFiltering,
} from './filtering-engines.js';
/******************************************************************************/
let lastVersionInt = 0;
let thisVersionInt = 0;
/******************************************************************************/
vAPI.app.onShutdown = ( ) => {
staticFilteringReverseLookup.shutdown();
io.updateStop();
staticNetFilteringEngine.reset();
staticExtFilteringEngine.reset();
sessionFirewall.reset();
permanentFirewall.reset();
sessionURLFiltering.reset();
permanentURLFiltering.reset();
sessionSwitches.reset();
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
// the extension was launched. It can be used to inject content scripts
// in already opened web pages, to remove whatever nuisance could make it to
// the web pages before uBlock was ready.
//
// https://bugzilla.mozilla.org/show_bug.cgi?id=1652925#c19
// Mind discarded tabs.
const initializeTabs = async ( ) => {
const manifest = browser.runtime.getManifest();
if ( manifest instanceof Object === false ) { return; }
const toCheck = [];
const tabIds = [];
{
const checker = { file: 'js/scriptlets/should-inject-contentscript.js' };
const tabs = await vAPI.tabs.query({ url: '<all_urls>' });
for ( const tab of tabs ) {
if ( tab.discarded === true ) { continue; }
if ( tab.status === 'unloaded' ) { continue; }
const { id, url } = tab;
µb.tabContextManager.commit(id, url);
µb.bindTabToPageStore(id, 'tabCommitted', tab);
// https://github.com/chrisaljoudi/uBlock/issues/129
// Find out whether content scripts need to be injected
// programmatically. This may be necessary for web pages which
// were loaded before uBO launched.
toCheck.push(
/^https?:\/\//.test(url)
? vAPI.tabs.executeScript(id, checker)
: false
);
tabIds.push(id);
}
}
// We do not want to block on content scripts injection
Promise.all(toCheck).then(results => {
for ( let i = 0; i < results.length; i++ ) {
const result = results[i];
if ( result.length === 0 || result[0] !== true ) { continue; }
// Inject declarative content scripts programmatically.
for ( const contentScript of manifest.content_scripts ) {
for ( const file of contentScript.js ) {
vAPI.tabs.executeScript(tabIds[i], {
file: file,
allFrames: contentScript.all_frames,
runAt: contentScript.run_at
});
}
}
}
});
};
/******************************************************************************/
// To bring older versions up to date
//
// https://www.reddit.com/r/uBlockOrigin/comments/s7c9go/
// Abort suspending network requests when uBO is merely being installed.
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(),
});
// 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);
};
/******************************************************************************/
// https://github.com/uBlockOrigin/uBlock-issues/issues/1433
// Allow admins to add their own trusted-site directives.
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 ) {
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();
};
/******************************************************************************/
// User settings are in memory
const onUserSettingsReady = fetched => {
// Terminate suspended state?
const tnow = Date.now() - vAPI.T0;
if (
vAPI.Net.canSuspend() &&
fetched.suspendUntilListsAreLoaded === false
) {
vAPI.net.unsuspend({ all: true, discard: true });
ubolog(`Unsuspend network activity listener at ${tnow} ms`);
µb.supportStats.unsuspendAfter = `${tnow} ms`;
} else if (
vAPI.Net.canSuspend() === false &&
fetched.suspendUntilListsAreLoaded
) {
vAPI.net.suspend();
ubolog(`Suspend network activity listener at ${tnow} ms`);
}
// `externalLists` will be deprecated in some future, it is kept around
// for forward compatibility purpose, and should reflect the content of
// `importedLists`.
if ( Array.isArray(fetched.externalLists) ) {
fetched.externalLists = fetched.externalLists.join('\n');
vAPI.storage.set({ externalLists: fetched.externalLists });
}
if (
fetched.importedLists.length === 0 &&
fetched.externalLists !== ''
) {
fetched.importedLists = fetched.externalLists.trim().split(/[\n\r]+/);
}
fromFetch(µb.userSettings, fetched);
if ( µb.privacySettingsSupported ) {
vAPI.browserSettings.set({
'hyperlinkAuditing': !µb.userSettings.hyperlinkAuditingDisabled,
'prefetching': !µb.userSettings.prefetchingDisabled,
'webrtcIPAddress': !µb.userSettings.webrtcIPAddressHidden
});
}
// https://github.com/uBlockOrigin/uBlock-issues/issues/1513
if (
vAPI.net.canUncloakCnames &&
µb.userSettings.cnameUncloakEnabled === false
) {
vAPI.net.setOptions({ cnameUncloakEnabled: false });
}
};
/******************************************************************************/
// https://bugzilla.mozilla.org/show_bug.cgi?id=1588916
// Save magic format numbers into the cache storage itself.
// https://github.com/uBlockOrigin/uBlock-issues/issues/1365
// 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;
selfieIsInvalid = true;
ubolog(`Serialized format of static filter lists changed`);
}
if ( fetched.selfieMagic !== µb.systemSettings.selfieMagic ) {
selfieIsInvalid = true;
ubolog(`Serialized format of selfie changed`);
}
if ( selfieIsInvalid === false ) { return; }
µb.selfieManager.destroy({ janitor: true });
cacheStorage.set(µb.systemSettings);
};
/******************************************************************************/
const onHiddenSettingsReady = async ( ) => {
// Maybe customize webext flavor
if ( µb.hiddenSettings.modifyWebextFlavor !== 'unset' ) {
const tokens = µb.hiddenSettings.modifyWebextFlavor.split(/\s+/);
for ( const token of tokens ) {
switch ( token[0] ) {
case '+':
vAPI.webextFlavor.soup.add(token.slice(1));
break;
case '-':
vAPI.webextFlavor.soup.delete(token.slice(1));
break;
default:
vAPI.webextFlavor.soup.add(token);
break;
}
}
ubolog(`Override default webext flavor with ${tokens}`);
}
// Maybe disable WebAssembly
if ( vAPI.canWASM && µb.hiddenSettings.disableWebAssembly !== true ) {
const wasmModuleFetcher = function(path) {
return fetch(`${path}.wasm`, { mode: 'same-origin' }).then(
WebAssembly.compileStreaming
).catch(reason => {
ubolog(reason);
});
};
staticNetFilteringEngine.enableWASM(wasmModuleFetcher, './js/wasm/').then(result => {
if ( result !== true ) { return; }
ubolog(`WASM modules ready ${Date.now()-vAPI.T0} ms after launch`);
});
}
};
/******************************************************************************/
const onFirstFetchReady = (fetched, adminExtra) => {
// https://github.com/uBlockOrigin/uBlock-issues/issues/507
// Firefox-specific: somehow `fetched` is undefined under certain
// circumstances even though we asked to load with default values.
if ( fetched instanceof Object === false ) {
fetched = createDefaultProps();
}
// Order is important -- do not change:
fromFetch(µb.restoreBackupSettings, fetched);
permanentFirewall.fromString(fetched.dynamicFilteringString);
sessionFirewall.assign(permanentFirewall);
permanentURLFiltering.fromString(fetched.urlFilteringString);
sessionURLFiltering.assign(permanentURLFiltering);
permanentSwitches.fromString(fetched.hostnameSwitchesString);
sessionSwitches.assign(permanentSwitches);
onNetWhitelistReady(fetched.netWhitelist, adminExtra);
};
/******************************************************************************/
const toFetch = (from, fetched) => {
for ( const k in from ) {
if ( from.hasOwnProperty(k) === false ) { continue; }
fetched[k] = from[k];
}
};
const fromFetch = (to, fetched) => {
for ( const k in to ) {
if ( to.hasOwnProperty(k) === false ) { continue; }
if ( fetched.hasOwnProperty(k) === false ) { continue; }
to[k] = fetched[k];
}
};
const createDefaultProps = ( ) => {
const fetchableProps = {
'dynamicFilteringString': µb.dynamicFilteringDefault.join('\n'),
'urlFilteringString': '',
'hostnameSwitchesString': µb.hostnameSwitchesDefault.join('\n'),
'netWhitelist': µb.netWhitelistDefault,
'version': '0.0.0.0'
};
toFetch(µb.restoreBackupSettings, fetchableProps);
return fetchableProps;
};
/******************************************************************************/
(async ( ) => {
// >>>>> start of async/await scope
try {
ubolog(`Start sequence of loading storage-based data ${Date.now()-vAPI.T0} ms after launch`);
// https://github.com/gorhill/uBlock/issues/531
await µb.restoreAdminSettings();
ubolog(`Admin settings ready ${Date.now()-vAPI.T0} ms after launch`);
await µb.loadHiddenSettings();
await onHiddenSettingsReady();
ubolog(`Hidden settings ready ${Date.now()-vAPI.T0} ms after launch`);
const adminExtra = await vAPI.adminStorage.get('toAdd');
ubolog(`Extra admin settings 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}`);
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`);
}),
µb.loadUserSettings().then(fetched => {
ubolog(`User settings ready ${Date.now()-vAPI.T0} ms after launch`);
onUserSettingsReady(fetched);
}),
µ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 ( lastVersionInt === 0 && vAPI.webextFlavor.soup.has('chromium') ) {
vAPI.app.restart();
return;
}
} catch (ex) {
console.trace(ex);
}
// Prime the filtering engines before first use.
staticNetFilteringEngine.prime();
// https://github.com/uBlockOrigin/uBlock-issues/issues/817#issuecomment-565730122
// Still try to load filter lists regardless of whether a serious error
// occurred in the previous initialization steps.
let selfieIsValid = false;
try {
selfieIsValid = await µb.selfieManager.load();
if ( selfieIsValid === true ) {
ubolog(`Loaded filtering engine from selfie ${Date.now()-vAPI.T0} ms after launch`);
}
} catch (ex) {
console.trace(ex);
}
if ( selfieIsValid !== true ) {
try {
await µb.loadFilterLists();
ubolog(`Filter lists ready ${Date.now()-vAPI.T0} ms after launch`);
} catch (ex) {
console.trace(ex);
}
}
// Flush memory cache -- unsure whether the browser does this internally
// when loading a new extension.
filteringBehaviorChanged();
// Final initialization steps after all needed assets are in memory.
// https://github.com/uBlockOrigin/uBlock-issues/issues/974
// This can be used to defer filtering decision-making.
µb.readyToFilter = true;
// Initialize internal state with maybe already existing tabs.
await initializeTabs();
// Start network observers.
webRequest.start();
// Force an update of the context menu according to the currently
// active tab.
contextMenu.update();
// https://github.com/uBlockOrigin/uBlock-issues/issues/717
// Prevent the extension from being restarted mid-session.
browser.runtime.onUpdateAvailable.addListener(details => {
const toInt = vAPI.app.intFromVersion;
if (
µb.hiddenSettings.extensionUpdateForceReload === true ||
toInt(details.version) <= toInt(vAPI.app.version)
) {
vAPI.app.restart();
}
});
µb.supportStats.allReadyAfter = `${Date.now() - vAPI.T0} ms`;
if ( selfieIsValid ) {
µb.supportStats.allReadyAfter += ' (selfie)';
}
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
})();

View File

@@ -0,0 +1,515 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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
*/
import * as sfp from './static-filtering-parser.js';
import {
CompiledListReader,
CompiledListWriter,
} from './static-filtering-io.js';
import { LineIterator } from './text-utils.js';
import staticNetFilteringEngine from './static-net-filtering.js';
/******************************************************************************/
// http://www.cse.yorku.ca/~oz/hash.html#djb2
// Must mirror content script surveyor's version
const hashFromStr = (type, s) => {
const len = s.length;
const step = len + 7 >>> 3;
let hash = (type << 5) + type ^ len;
for ( let i = 0; i < len; i += step ) {
hash = (hash << 5) + hash ^ s.charCodeAt(i);
}
return hash & 0xFFFFFF;
};
const isRegex = hn => hn.startsWith('/') && hn.endsWith('/');
/******************************************************************************/
// Copied from cosmetic-filter.js for the time being to avoid unwanted
// dependencies
const rePlainSelector = /^[#.][\w\\-]+/;
const rePlainSelectorEx = /^[^#.[(]+([#.][\w-]+)|([#.][\w-]+)$/;
const rePlainSelectorEscaped = /^[#.](?:\\[0-9A-Fa-f]+ |\\.|\w|-)+/;
const reEscapeSequence = /\\([0-9A-Fa-f]+ |.)/g;
const keyFromSelector = selector => {
let key = '';
let matches = rePlainSelector.exec(selector);
if ( matches ) {
key = matches[0];
} else {
matches = rePlainSelectorEx.exec(selector);
if ( matches === null ) { return; }
key = matches[1] || matches[2];
}
if ( key.indexOf('\\') === -1 ) { return key; }
matches = rePlainSelectorEscaped.exec(selector);
if ( matches === null ) { return; }
key = '';
const escaped = matches[0];
let beg = 0;
reEscapeSequence.lastIndex = 0;
for (;;) {
matches = reEscapeSequence.exec(escaped);
if ( matches === null ) {
return key + escaped.slice(beg);
}
key += escaped.slice(beg, matches.index);
beg = reEscapeSequence.lastIndex;
if ( matches[1].length === 1 ) {
key += matches[1];
} else {
key += String.fromCharCode(parseInt(matches[1], 16));
}
}
};
/******************************************************************************/
function addGenericCosmeticFilter(context, selector, isException) {
if ( selector === undefined ) { return; }
if ( selector.length <= 1 ) { return; }
if ( isException ) {
if ( context.genericCosmeticExceptions === undefined ) {
context.genericCosmeticExceptions = new Set();
}
context.genericCosmeticExceptions.add(selector);
return;
}
if ( selector.charCodeAt(0) === 0x7B /* '{' */ ) { return; }
const key = keyFromSelector(selector);
if ( key === undefined ) {
if ( context.genericHighCosmeticFilters === undefined ) {
context.genericHighCosmeticFilters = new Set();
}
context.genericHighCosmeticFilters.add(selector);
return;
}
const type = key.charCodeAt(0);
const hash = hashFromStr(type, key.slice(1));
if ( context.genericCosmeticFilters === undefined ) {
context.genericCosmeticFilters = new Map();
}
let bucket = context.genericCosmeticFilters.get(hash);
if ( bucket === undefined ) {
context.genericCosmeticFilters.set(hash, bucket = []);
}
bucket.push(selector);
}
/******************************************************************************/
function addExtendedToDNR(context, parser) {
if ( parser.isExtendedFilter() === false ) { return false; }
// Scriptlet injection
if ( parser.isScriptletFilter() ) {
if ( parser.hasOptions() === false ) { return; }
if ( context.scriptletFilters === undefined ) {
context.scriptletFilters = new Map();
}
const exception = parser.isException();
const args = parser.getScriptletArgs();
const argsToken = JSON.stringify(args);
for ( const { hn, not, bad } of parser.getExtFilterDomainIterator() ) {
if ( bad ) { continue; }
if ( exception ) { continue; }
if ( isRegex(hn) ) { continue; }
let details = context.scriptletFilters.get(argsToken);
if ( details === undefined ) {
context.scriptletFilters.set(argsToken, details = { args });
if ( context.trustedSource ) {
details.trustedSource = true;
}
}
if ( not ) {
if ( details.excludeMatches === undefined ) {
details.excludeMatches = [];
}
details.excludeMatches.push(hn);
continue;
}
if ( details.matches === undefined ) {
details.matches = [];
}
if ( details.matches.includes('*') ) { continue; }
if ( hn === '*' ) {
details.matches = [ '*' ];
continue;
}
details.matches.push(hn);
}
return;
}
// Response header filtering
if ( parser.isResponseheaderFilter() ) {
if ( parser.hasError() ) { return; }
if ( parser.hasOptions() === false ) { return; }
if ( parser.isException() ) { return; }
const node = parser.getBranchFromType(sfp.NODE_TYPE_EXT_PATTERN_RESPONSEHEADER);
if ( node === 0 ) { return; }
const header = parser.getNodeString(node);
if ( context.responseHeaderRules === undefined ) {
context.responseHeaderRules = [];
}
const rule = {
action: {
responseHeaders: [
{
header,
operation: 'remove',
}
],
type: 'modifyHeaders'
},
condition: {
resourceTypes: [
'main_frame',
'sub_frame'
]
},
};
for ( const { hn, not, bad } of parser.getExtFilterDomainIterator() ) {
if ( bad ) { continue; }
if ( isRegex(hn) ) { continue; }
if ( not ) {
if ( rule.condition.excludedInitiatorDomains === undefined ) {
rule.condition.excludedInitiatorDomains = [];
}
rule.condition.excludedInitiatorDomains.push(hn);
continue;
}
if ( hn === '*' ) {
if ( rule.condition.initiatorDomains !== undefined ) {
rule.condition.initiatorDomains = undefined;
}
continue;
}
if ( rule.condition.initiatorDomains === undefined ) {
rule.condition.initiatorDomains = [];
}
rule.condition.initiatorDomains.push(hn);
}
context.responseHeaderRules.push(rule);
return;
}
// HTML filtering
if ( (parser.flavorBits & parser.BITFlavorExtHTML) !== 0 ) {
return;
}
// Cosmetic filtering
// Generic cosmetic filtering
if ( parser.hasOptions() === false ) {
const { compiled, exception } = parser.result;
addGenericCosmeticFilter(context, compiled, exception);
return;
}
// Specific cosmetic filtering
// https://github.com/chrisaljoudi/uBlock/issues/151
// Negated hostname means the filter applies to all non-negated hostnames
// of same filter OR globally if there is no non-negated hostnames.
if ( context.specificCosmeticFilters === undefined ) {
context.specificCosmeticFilters = new Map();
}
const { compiled, exception, raw } = parser.result;
if ( compiled === undefined ) {
context.specificCosmeticFilters.set(`Invalid filter: ...##${raw}`, {
rejected: true
});
return;
}
let details = context.specificCosmeticFilters.get(compiled);
for ( const { hn, not, bad } of parser.getExtFilterDomainIterator() ) {
if ( bad ) { continue; }
if ( not && exception ) { continue; }
if ( isRegex(hn) ) { continue; }
if ( details === undefined ) {
context.specificCosmeticFilters.set(compiled, details = {});
}
if ( exception ) {
if ( details.excludeMatches === undefined ) {
details.excludeMatches = [];
}
details.excludeMatches.push(hn);
continue;
}
if ( details.matches === undefined ) {
details.matches = [];
}
if ( details.matches.includes('*') ) { continue; }
if ( hn === '*' ) {
details.matches = [ '*' ];
continue;
}
details.matches.push(hn);
}
if ( details === undefined ) { return; }
if ( exception ) { return; }
if ( compiled.startsWith('{') ) { return; }
if ( details.matches === undefined || details.matches.includes('*') ) {
addGenericCosmeticFilter(context, compiled, false);
details.matches = undefined;
}
}
/******************************************************************************/
function addToDNR(context, list) {
const env = context.env || [];
const writer = new CompiledListWriter();
const lineIter = new LineIterator(
sfp.utils.preparser.prune(list.text, env)
);
const parser = new sfp.AstFilterParser({
toDNR: true,
nativeCssHas: env.includes('native_css_has'),
badTypes: [ sfp.NODE_TYPE_NET_OPTION_NAME_REDIRECTRULE ],
trustedSource: list.trustedSource || undefined,
});
const compiler = staticNetFilteringEngine.createCompiler();
writer.properties.set('name', list.name);
compiler.start(writer);
while ( lineIter.eot() === false ) {
let line = lineIter.next();
while ( line.endsWith(' \\') ) {
if ( lineIter.peek(4) !== ' ' ) { break; }
line = line.slice(0, -2).trim() + lineIter.next().trim();
}
parser.parse(line);
if ( parser.isComment() ) {
if ( line === `!#trusted on ${context.secret}` ) {
parser.options.trustedSource = true;
context.trustedSource = true;
} else if ( line === `!#trusted off ${context.secret}` ) {
parser.options.trustedSource = false;
context.trustedSource = false;
}
continue;
}
if ( parser.isFilter() === false ) { continue; }
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;
}
if ( parser.isExtendedFilter() ) {
addExtendedToDNR(context, parser);
continue;
}
if ( parser.isNetworkFilter() === false ) { continue; }
if ( compiler.compile(parser, writer) ) { continue; }
if ( compiler.error !== undefined ) {
context.invalid.add(compiler.error);
}
}
compiler.finish(writer);
staticNetFilteringEngine.dnrFromCompiled(
'add',
context,
new CompiledListReader(writer.toString())
);
}
/******************************************************************************/
// Merge rules where possible by merging arrays of a specific property.
//
// https://github.com/uBlockOrigin/uBOL-home/issues/10#issuecomment-1304822579
// Do not merge rules which have errors.
function mergeRules(rulesetMap, mergeTarget) {
const sorter = (_, v) => {
if ( Array.isArray(v) ) {
return typeof v[0] === 'string' ? v.sort() : v;
}
if ( v instanceof Object ) {
const sorted = {};
for ( const kk of Object.keys(v).sort() ) {
sorted[kk] = v[kk];
}
return sorted;
}
return v;
};
const ruleHasher = (rule, target) => {
return JSON.stringify(rule, (k, v) => {
if ( k.startsWith('_') ) { return; }
if ( k === target ) { return; }
return sorter(k, v);
});
};
const extractTargetValue = (obj, target) => {
for ( const [ k, v ] of Object.entries(obj) ) {
if ( Array.isArray(v) && k === target ) { return v; }
if ( v instanceof Object ) {
const r = extractTargetValue(v, target);
if ( r !== undefined ) { return r; }
}
}
};
const extractTargetOwner = (obj, target) => {
for ( const [ k, v ] of Object.entries(obj) ) {
if ( Array.isArray(v) && k === target ) { return obj; }
if ( v instanceof Object ) {
const r = extractTargetOwner(v, target);
if ( r !== undefined ) { return r; }
}
}
};
const mergeMap = new Map();
for ( const [ id, rule ] of rulesetMap ) {
if ( rule._error !== undefined ) { continue; }
const hash = ruleHasher(rule, mergeTarget);
if ( mergeMap.has(hash) === false ) {
mergeMap.set(hash, []);
}
mergeMap.get(hash).push(id);
}
for ( const ids of mergeMap.values() ) {
if ( ids.length === 1 ) { continue; }
const leftHand = rulesetMap.get(ids[0]);
const leftHandSet = new Set(
extractTargetValue(leftHand, mergeTarget) || []
);
for ( let i = 1; i < ids.length; i++ ) {
const rightHandId = ids[i];
const rightHand = rulesetMap.get(rightHandId);
const rightHandArray = extractTargetValue(rightHand, mergeTarget);
if ( rightHandArray !== undefined ) {
if ( leftHandSet.size !== 0 ) {
for ( const item of rightHandArray ) {
leftHandSet.add(item);
}
}
} else {
leftHandSet.clear();
}
rulesetMap.delete(rightHandId);
}
const leftHandOwner = extractTargetOwner(leftHand, mergeTarget);
if ( leftHandSet.size > 1 ) {
//if ( leftHandOwner === undefined ) { debugger; }
leftHandOwner[mergeTarget] = Array.from(leftHandSet).sort();
} else if ( leftHandSet.size === 0 ) {
if ( leftHandOwner !== undefined ) {
leftHandOwner[mergeTarget] = undefined;
}
}
}
}
/******************************************************************************/
function finalizeRuleset(context, network) {
const ruleset = network.ruleset;
// Assign rule ids
const rulesetMap = new Map();
{
let ruleId = 1;
for ( const rule of ruleset ) {
rulesetMap.set(ruleId++, rule);
}
}
mergeRules(rulesetMap, 'resourceTypes');
mergeRules(rulesetMap, 'removeParams');
mergeRules(rulesetMap, 'initiatorDomains');
mergeRules(rulesetMap, 'requestDomains');
mergeRules(rulesetMap, 'responseHeaders');
// Patch id
const rulesetFinal = [];
{
let ruleId = 1;
for ( const rule of rulesetMap.values() ) {
if ( rule._error === undefined ) {
rule.id = ruleId++;
} else {
rule.id = 0;
}
rulesetFinal.push(rule);
}
for ( const invalid of context.invalid ) {
rulesetFinal.push({ _error: [ invalid ] });
}
}
network.ruleset = rulesetFinal;
}
/******************************************************************************/
async function dnrRulesetFromRawLists(lists, options = {}) {
const context = Object.assign({}, options);
staticNetFilteringEngine.dnrFromCompiled('begin', context);
context.extensionPaths = new Map(context.extensionPaths || []);
const toLoad = [];
const toDNR = (context, list) => addToDNR(context, list);
for ( const list of lists ) {
if ( list instanceof Promise ) {
toLoad.push(list.then(list => toDNR(context, list)));
} else {
toLoad.push(toDNR(context, list));
}
}
await Promise.all(toLoad);
const result = {
network: staticNetFilteringEngine.dnrFromCompiled('end', context),
genericCosmetic: context.genericCosmeticFilters,
genericHighCosmetic: context.genericHighCosmeticFilters,
genericCosmeticExceptions: context.genericCosmeticExceptions,
specificCosmetic: context.specificCosmeticFilters,
scriptlet: context.scriptletFilters,
};
if ( context.responseHeaderRules ) {
result.network.ruleset.push(...context.responseHeaderRules);
}
finalizeRuleset(context, result.network);
return result;
}
/******************************************************************************/
export { dnrRulesetFromRawLists, mergeRules };

View File

@@ -0,0 +1,171 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2017-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';
/******************************************************************************/
const StaticExtFilteringHostnameDB = class {
constructor(nBits, version = 0) {
this.version = version;
this.nBits = nBits;
this.strToIdMap = new Map();
this.hostnameToSlotIdMap = new Map();
this.regexToSlotIdMap = new Map();
this.regexMap = new Map();
// Array of integer pairs
this.hostnameSlots = [];
// Array of strings (selectors and pseudo-selectors)
this.strSlots = [];
this.size = 0;
this.cleanupTimer = vAPI.defer.create(( ) => {
this.strToIdMap.clear();
});
}
store(hn, bits, s) {
this.size += 1;
let iStr = this.strToIdMap.get(s);
if ( iStr === undefined ) {
iStr = this.strSlots.length;
this.strSlots.push(s);
this.strToIdMap.set(s, iStr);
if ( this.cleanupTimer.ongoing() === false ) {
this.collectGarbage(true);
}
}
const strId = iStr << this.nBits | bits;
const hnIsNotRegex = hn.charCodeAt(0) !== 0x2F /* / */;
let iHn = hnIsNotRegex
? this.hostnameToSlotIdMap.get(hn)
: this.regexToSlotIdMap.get(hn);
if ( iHn === undefined ) {
if ( hnIsNotRegex ) {
this.hostnameToSlotIdMap.set(hn, this.hostnameSlots.length);
} else {
this.regexToSlotIdMap.set(hn, this.hostnameSlots.length);
}
this.hostnameSlots.push(strId, 0);
return;
}
// Add as last item.
while ( this.hostnameSlots[iHn+1] !== 0 ) {
iHn = this.hostnameSlots[iHn+1];
}
this.hostnameSlots[iHn+1] = this.hostnameSlots.length;
this.hostnameSlots.push(strId, 0);
}
clear() {
this.hostnameToSlotIdMap.clear();
this.regexToSlotIdMap.clear();
this.hostnameSlots.length = 0;
this.strSlots.length = 0;
this.strToIdMap.clear();
this.regexMap.clear();
this.size = 0;
}
collectGarbage(later = false) {
if ( later ) {
return this.cleanupTimer.onidle(5000, { timeout: 5000 });
}
this.cleanupTimer.off();
this.strToIdMap.clear();
}
// modifiers = 0: all items
// modifiers = 1: only specific items
// modifiers = 2: only generic items
// modifiers = 3: only regex-based items
//
retrieve(hostname, out, modifiers = 0) {
let hn = hostname;
if ( modifiers === 2 ) { hn = ''; }
for (;;) {
const hnSlot = this.hostnameToSlotIdMap.get(hn);
if ( hnSlot !== undefined ) {
this.retrieveFromSlot(hnSlot, out);
}
if ( hn === '' ) { break; }
const pos = hn.indexOf('.');
if ( pos === -1 ) {
if ( modifiers === 1 ) { break; }
hn = '';
} else {
hn = hn.slice(pos + 1);
}
}
if ( modifiers !== 0 && modifiers !== 3 ) { return; }
if ( this.regexToSlotIdMap.size === 0 ) { return; }
// TODO: consider using a combined regex to test once for whether
// iterating is worth it.
for ( const restr of this.regexToSlotIdMap.keys() ) {
let re = this.regexMap.get(restr);
if ( re === undefined ) {
this.regexMap.set(restr, (re = new RegExp(restr.slice(1,-1))));
}
if ( re.test(hostname) === false ) { continue; }
this.retrieveFromSlot(this.regexToSlotIdMap.get(restr), out);
}
}
retrieveFromSlot(hnSlot, out) {
if ( hnSlot === undefined ) { return; }
const mask = out.length - 1; // out.length must be power of two
do {
const strId = this.hostnameSlots[hnSlot+0];
out[strId & mask].add(this.strSlots[strId >>> this.nBits]);
hnSlot = this.hostnameSlots[hnSlot+1];
} while ( hnSlot !== 0 );
}
toSelfie() {
return {
version: this.version,
hostnameToSlotIdMap: this.hostnameToSlotIdMap,
regexToSlotIdMap: this.regexToSlotIdMap,
hostnameSlots: this.hostnameSlots,
strSlots: this.strSlots,
size: this.size
};
}
fromSelfie(selfie) {
if ( typeof selfie !== 'object' || selfie === null ) { return; }
this.hostnameToSlotIdMap = selfie.hostnameToSlotIdMap;
// Regex-based lookup available in uBO 1.47.0 and above
if ( selfie.regexToSlotIdMap ) {
this.regexToSlotIdMap = selfie.regexToSlotIdMap;
}
this.hostnameSlots = selfie.hostnameSlots;
this.strSlots = selfie.strSlots;
this.size = selfie.size;
}
};
/******************************************************************************/
export {
StaticExtFilteringHostnameDB,
};
/******************************************************************************/

View File

@@ -0,0 +1,173 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2017-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';
/******************************************************************************/
import cosmeticFilteringEngine from './cosmetic-filtering.js';
import htmlFilteringEngine from './html-filtering.js';
import httpheaderFilteringEngine from './httpheader-filtering.js';
import scriptletFilteringEngine from './scriptlet-filtering.js';
import logger from './logger.js';
/*******************************************************************************
All static extended filters are of the form:
field 1: one hostname, or a list of comma-separated hostnames
field 2: `##` or `#@#`
field 3: selector
The purpose of the static extended filtering engine is to coarse-parse and
dispatch to appropriate specialized filtering engines. There are currently
three specialized filtering engines:
- cosmetic filtering (aka "element hiding" in Adblock Plus)
- scriptlet injection: selector starts with `script:inject`
- New shorter syntax (1.15.12): `example.com##+js(bab-defuser.js)`
- html filtering: selector starts with `^`
Depending on the specialized filtering engine, field 1 may or may not be
optional.
The static extended filtering engine also offers parsing capabilities which
are available to all other specialized filtering engines. For example,
cosmetic and html filtering can ask the extended filtering engine to
compile/validate selectors.
**/
//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------
const staticExtFilteringEngine = {
get acceptedCount() {
return cosmeticFilteringEngine.acceptedCount +
scriptletFilteringEngine.acceptedCount +
httpheaderFilteringEngine.acceptedCount +
htmlFilteringEngine.acceptedCount;
},
get discardedCount() {
return cosmeticFilteringEngine.discardedCount +
scriptletFilteringEngine.discardedCount +
httpheaderFilteringEngine.discardedCount +
htmlFilteringEngine.discardedCount;
},
};
//--------------------------------------------------------------------------
// Public methods
//--------------------------------------------------------------------------
staticExtFilteringEngine.reset = function() {
cosmeticFilteringEngine.reset();
scriptletFilteringEngine.reset();
httpheaderFilteringEngine.reset();
htmlFilteringEngine.reset();
};
staticExtFilteringEngine.freeze = function() {
cosmeticFilteringEngine.freeze();
scriptletFilteringEngine.freeze();
httpheaderFilteringEngine.freeze();
htmlFilteringEngine.freeze();
};
staticExtFilteringEngine.compile = function(parser, writer) {
if ( parser.isExtendedFilter() === false ) { return false; }
if ( parser.hasError() ) {
logger.writeOne({
realm: 'message',
type: 'error',
text: `Invalid extended filter in ${writer.properties.get('name') || '?'}: ${parser.raw}`
});
return true;
}
// Scriptlet injection
if ( parser.isScriptletFilter() ) {
scriptletFilteringEngine.compile(parser, writer);
return true;
}
// Response header filtering
if ( parser.isResponseheaderFilter() ) {
httpheaderFilteringEngine.compile(parser, writer);
return true;
}
// HTML filtering
// TODO: evaluate converting Adguard's `$$` syntax into uBO's HTML
// filtering syntax.
if ( parser.isHtmlFilter() ) {
htmlFilteringEngine.compile(parser, writer);
return true;
}
// Cosmetic filtering
if ( parser.isCosmeticFilter() ) {
cosmeticFilteringEngine.compile(parser, writer);
return true;
}
logger.writeOne({
realm: 'message',
type: 'error',
text: `Unknown extended filter in ${writer.properties.get('name') || '?'}: ${parser.raw}`
});
return true;
};
staticExtFilteringEngine.fromCompiledContent = function(reader, options) {
cosmeticFilteringEngine.fromCompiledContent(reader, options);
scriptletFilteringEngine.fromCompiledContent(reader, options);
httpheaderFilteringEngine.fromCompiledContent(reader, options);
htmlFilteringEngine.fromCompiledContent(reader, options);
};
staticExtFilteringEngine.toSelfie = function() {
return {
cosmetic: cosmeticFilteringEngine.toSelfie(),
scriptlets: scriptletFilteringEngine.toSelfie(),
httpHeaders: httpheaderFilteringEngine.toSelfie(),
html: htmlFilteringEngine.toSelfie(),
};
};
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;
};
/******************************************************************************/
export default staticExtFilteringEngine;
/******************************************************************************/

View File

@@ -0,0 +1,144 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-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';
/******************************************************************************/
// https://www.reddit.com/r/uBlockOrigin/comments/oq6kt5/ubo_loads_generic_filter_instead_of_specific/
// Ensure blocks of content are sorted in ascending id order, such that the
// specific cosmetic filters will be found (and thus reported) before the
// generic ones.
const serialize = JSON.stringify;
const unserialize = JSON.parse;
const blockStartPrefix = '#block-start-'; // ensure no special regex characters
const blockEndPrefix = '#block-end-'; // ensure no special regex characters
class CompiledListWriter {
constructor() {
this.blockId = undefined;
this.block = undefined;
this.blocks = new Map();
this.properties = new Map();
}
push(args) {
this.block.push(serialize(args));
}
pushMany(many) {
for ( const args of many ) {
this.block.push(serialize(args));
}
}
last() {
if ( Array.isArray(this.block) && this.block.length !== 0 ) {
return this.block[this.block.length - 1];
}
}
select(blockId) {
if ( blockId === this.blockId ) { return; }
this.blockId = blockId;
this.block = this.blocks.get(blockId);
if ( this.block === undefined ) {
this.blocks.set(blockId, (this.block = []));
}
return this;
}
toString() {
const result = [];
const sortedBlocks =
Array.from(this.blocks).sort((a, b) => a[0] - b[0]);
for ( const [ id, lines ] of sortedBlocks ) {
if ( lines.length === 0 ) { continue; }
result.push(
blockStartPrefix + id,
lines.join('\n'),
blockEndPrefix + id
);
}
return result.join('\n');
}
static serialize(arg) {
return serialize(arg);
}
}
class CompiledListReader {
constructor(raw, blockId) {
this.block = '';
this.len = 0;
this.offset = 0;
this.line = '';
this.blocks = new Map();
this.properties = new Map();
const reBlockStart = new RegExp(`^${blockStartPrefix}([\\w:]+)\\n`, 'gm');
let match = reBlockStart.exec(raw);
while ( match !== null ) {
const sectionId = match[1];
const beg = match.index + match[0].length;
const end = raw.indexOf(blockEndPrefix + sectionId, beg);
this.blocks.set(sectionId, raw.slice(beg, end));
reBlockStart.lastIndex = end;
match = reBlockStart.exec(raw);
}
if ( blockId !== undefined ) {
this.select(blockId);
}
}
next() {
if ( this.offset === this.len ) {
this.line = '';
return false;
}
let pos = this.block.indexOf('\n', this.offset);
if ( pos !== -1 ) {
this.line = this.block.slice(this.offset, pos);
this.offset = pos + 1;
} else {
this.line = this.block.slice(this.offset);
this.offset = this.len;
}
return true;
}
select(blockId) {
this.block = this.blocks.get(blockId) || '';
this.len = this.block.length;
this.offset = 0;
return this;
}
fingerprint() {
return this.line;
}
args() {
return unserialize(this.line);
}
static unserialize(arg) {
return unserialize(arg);
}
}
/******************************************************************************/
export {
CompiledListReader,
CompiledListWriter,
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More