summaryrefslogtreecommitdiff
path: root/data/extensions/uBlock0@raymondhill.net/js/3p-filters.js
diff options
context:
space:
mode:
Diffstat (limited to 'data/extensions/uBlock0@raymondhill.net/js/3p-filters.js')
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/3p-filters.js902
1 files changed, 902 insertions, 0 deletions
diff --git a/data/extensions/uBlock0@raymondhill.net/js/3p-filters.js b/data/extensions/uBlock0@raymondhill.net/js/3p-filters.js
new file mode 100644
index 0000000..fed3154
--- /dev/null
+++ b/data/extensions/uBlock0@raymondhill.net/js/3p-filters.js
@@ -0,0 +1,902 @@
+/*******************************************************************************
+
+ 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
+
+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 ( Object.hasOwn(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 ( Object.hasOwn(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();
+});
+
+/******************************************************************************/