diff options
Diffstat (limited to 'data/extensions/spyblock@gnu.org/lib/elemHide.js')
-rw-r--r-- | data/extensions/spyblock@gnu.org/lib/elemHide.js | 472 |
1 files changed, 238 insertions, 234 deletions
diff --git a/data/extensions/spyblock@gnu.org/lib/elemHide.js b/data/extensions/spyblock@gnu.org/lib/elemHide.js index b762040..a91f1d4 100644 --- a/data/extensions/spyblock@gnu.org/lib/elemHide.js +++ b/data/extensions/spyblock@gnu.org/lib/elemHide.js @@ -1,6 +1,6 @@ /* * This file is part of Adblock Plus <https://adblockplus.org/>, - * Copyright (C) 2006-2015 Eyeo GmbH + * Copyright (C) 2006-2017 eyeo GmbH * * Adblock Plus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -15,115 +15,135 @@ * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. */ +"use strict"; + /** * @fileOverview Element hiding implementation. */ -Cu.import("resource://gre/modules/Services.jsm"); - -let {Utils} = require("utils"); -let {IO} = require("io"); -let {Prefs} = require("prefs"); -let {ElemHideException} = require("filterClasses"); -let {FilterNotifier} = require("filterNotifier"); -let {AboutHandler} = require("elemHideHitRegistration"); +const {ElemHideException} = require("filterClasses"); +const {FilterNotifier} = require("filterNotifier"); /** * Lookup table, filters by their associated key - * @type Object + * @type {Object} */ -let filterByKey = Object.create(null); +let filterByKey = []; /** * Lookup table, keys of the filters by filter text - * @type Object + * @type {Object} */ let keyByFilter = Object.create(null); /** - * Lookup table, keys are known element hiding exceptions - * @type Object + * Nested lookup table, filter (or false if inactive) by filter key by domain. + * (Only contains filters that aren't unconditionally matched for all domains.) + * @type {Object} */ -let knownExceptions = Object.create(null); +let filtersByDomain = Object.create(null); /** - * Lookup table, lists of element hiding exceptions by selector - * @type Object + * Lookup table, filter key by selector. (Only used for selectors that are + * unconditionally matched for all domains.) */ -let exceptions = Object.create(null); +let filterKeyBySelector = Object.create(null); /** - * Currently applied stylesheet URL - * @type nsIURI + * This array caches the keys of filterKeyBySelector table (selectors which + * unconditionally apply on all domains). It will be null if the cache needs to + * be rebuilt. */ -let styleURL = null; +let unconditionalSelectors = null; /** - * Element hiding component - * @class + * This array caches the values of filterKeyBySelector table (filterIds for + * selectors which unconditionally apply on all domains). It will be null if the + * cache needs to be rebuilt. */ -let ElemHide = exports.ElemHide = -{ - /** - * Indicates whether filters have been added or removed since the last apply() call. - * @type Boolean - */ - isDirty: false, +let unconditionalFilterKeys = null; - /** - * Inidicates whether the element hiding stylesheet is currently applied. - * @type Boolean - */ - applied: false, +/** + * Object to be used instead when a filter has a blank domains property. + */ +let defaultDomains = Object.create(null); +defaultDomains[""] = true; - /** - * Called on module startup. - */ - init: function() - { - Prefs.addListener(function(name) - { - if (name == "enabled") - ElemHide.apply(); - }); - onShutdown.add(function() - { - ElemHide.unapply(); - }); +/** + * Lookup table, keys are known element hiding exceptions + * @type {Object} + */ +let knownExceptions = Object.create(null); - let styleFile = IO.resolveFilePath(Prefs.data_directory); - styleFile.append("elemhide.css"); - styleURL = Services.io.newFileURI(styleFile).QueryInterface(Ci.nsIFileURL); - }, +/** + * Lookup table, lists of element hiding exceptions by selector + * @type {Object} + */ +let exceptions = Object.create(null); +/** + * Container for element hiding filters + * @class + */ +let ElemHide = exports.ElemHide = { /** * Removes all known filters */ - clear: function() + clear() { - filterByKey = Object.create(null); + filterByKey = []; keyByFilter = Object.create(null); + filtersByDomain = Object.create(null); + filterKeyBySelector = Object.create(null); + unconditionalSelectors = unconditionalFilterKeys = null; knownExceptions = Object.create(null); exceptions = Object.create(null); - ElemHide.isDirty = false; - ElemHide.unapply(); + FilterNotifier.emit("elemhideupdate"); + }, + + _addToFiltersByDomain(key, filter) + { + let domains = filter.domains || defaultDomains; + for (let domain in domains) + { + let filters = filtersByDomain[domain]; + if (!filters) + filters = filtersByDomain[domain] = Object.create(null); + + if (domains[domain]) + filters[key] = filter; + else + filters[key] = false; + } }, /** * Add a new element hiding filter * @param {ElemHideFilter} filter */ - add: function(filter) + add(filter) { if (filter instanceof ElemHideException) { if (filter.text in knownExceptions) return; - let selector = filter.selector; + let {selector} = filter; if (!(selector in exceptions)) exceptions[selector] = []; exceptions[selector].push(filter); + + // If this is the first exception for a previously unconditionally + // applied element hiding selector we need to take care to update the + // lookups. + let filterKey = filterKeyBySelector[selector]; + if (typeof filterKey != "undefined") + { + this._addToFiltersByDomain(filterKey, filterByKey[filterKey]); + delete filterKeyBySelector[selector]; + unconditionalSelectors = unconditionalFilterKeys = null; + } + knownExceptions[filter.text] = true; } else @@ -131,14 +151,42 @@ let ElemHide = exports.ElemHide = if (filter.text in keyByFilter) return; - let key; - do { - key = Math.random().toFixed(15).substr(5); - } while (key in filterByKey); - - filterByKey[key] = filter; + let key = filterByKey.push(filter) - 1; keyByFilter[filter.text] = key; - ElemHide.isDirty = true; + + if (!(filter.domains || filter.selector in exceptions)) + { + // The new filter's selector is unconditionally applied to all domains + filterKeyBySelector[filter.selector] = key; + unconditionalSelectors = unconditionalFilterKeys = null; + } + else + { + // The new filter's selector only applies to some domains + this._addToFiltersByDomain(key, filter); + } + } + + FilterNotifier.emit("elemhideupdate"); + }, + + _removeFilterKey(key, filter) + { + if (filterKeyBySelector[filter.selector] == key) + { + delete filterKeyBySelector[filter.selector]; + unconditionalSelectors = unconditionalFilterKeys = null; + return; + } + + // We haven't found this filter in unconditional filters, look in + // filtersByDomain. + let domains = filter.domains || defaultDomains; + for (let domain in domains) + { + let filters = filtersByDomain[domain]; + if (filters) + delete filters[key]; } }, @@ -146,7 +194,7 @@ let ElemHide = exports.ElemHide = * Removes an element hiding filter * @param {ElemHideFilter} filter */ - remove: function(filter) + remove(filter) { if (filter instanceof ElemHideException) { @@ -167,226 +215,182 @@ let ElemHide = exports.ElemHide = let key = keyByFilter[filter.text]; delete filterByKey[key]; delete keyByFilter[filter.text]; - ElemHide.isDirty = true; + this._removeFilterKey(key, filter); } + + FilterNotifier.emit("elemhideupdate"); }, /** * Checks whether an exception rule is registered for a filter on a particular * domain. + * @param {Filter} filter + * @param {string} docDomain + * @return {ElemHideException} */ - getException: function(/**Filter*/ filter, /**String*/ docDomain) /**ElemHideException*/ + getException(filter, docDomain) { if (!(filter.selector in exceptions)) return null; let list = exceptions[filter.selector]; for (let i = list.length - 1; i >= 0; i--) + { if (list[i].isActiveOnDomain(docDomain)) return list[i]; + } return null; }, /** - * Will be set to true if apply() is running (reentrance protection). - * @type Boolean - */ - _applying: false, - - /** - * Will be set to true if an apply() call arrives while apply() is already - * running (delayed execution). - * @type Boolean - */ - _needsApply: false, - - /** - * Generates stylesheet URL and applies it globally + * Retrieves an element hiding filter by the corresponding protocol key + * @param {number} key + * @return {Filter} */ - apply: function() + getFilterByKey(key) { - if (this._applying) - { - this._needsApply = true; - return; - } - - if (!ElemHide.isDirty || !Prefs.enabled) - { - // Nothing changed, looks like we merely got enabled/disabled - if (Prefs.enabled && !ElemHide.applied) - { - try - { - Utils.styleService.loadAndRegisterSheet(styleURL, Ci.nsIStyleSheetService.USER_SHEET); - ElemHide.applied = true; - } - catch (e) - { - Cu.reportError(e); - } - } - else if (!Prefs.enabled && ElemHide.applied) - { - ElemHide.unapply(); - } - - return; - } - - IO.writeToFile(styleURL.file, this._generateCSSContent(), function(e) - { - this._applying = false; - - // _generateCSSContent is throwing NS_ERROR_NOT_AVAILABLE to indicate that - // there are no filters. If that exception is passed through XPCOM we will - // see a proper exception here, otherwise a number. - let noFilters = (e == Cr.NS_ERROR_NOT_AVAILABLE || (e && e.result == Cr.NS_ERROR_NOT_AVAILABLE)); - if (noFilters) - { - e = null; - IO.removeFile(styleURL.file, function(e) {}); - } - else if (e) - Cu.reportError(e); - - if (this._needsApply) - { - this._needsApply = false; - this.apply(); - } - else if (!e) - { - ElemHide.isDirty = false; - - ElemHide.unapply(); - - if (!noFilters) - { - try - { - Utils.styleService.loadAndRegisterSheet(styleURL, Ci.nsIStyleSheetService.USER_SHEET); - ElemHide.applied = true; - } - catch (e) - { - Cu.reportError(e); - } - } - - FilterNotifier.triggerListeners("elemhideupdate"); - } - }.bind(this)); - - this._applying = true; + return (key in filterByKey ? filterByKey[key] : null); }, - _generateCSSContent: function() + /** + * Returns a list of all selectors as a nested map. On first level, the keys + * are all values of `ElemHideBase.selectorDomain` (domains on which these + * selectors should apply, ignoring exceptions). The values are maps again, + * with the keys being selectors and values the corresponding filter keys. + * @returns {Map.<String,Map<String,String>>} + */ + getSelectors() { - // Grouping selectors by domains - let domains = Object.create(null); - let hasFilters = false; + let domains = new Map(); for (let key in filterByKey) { let filter = filterByKey[key]; - let domain = filter.selectorDomain || ""; - - let list; - if (domain in domains) - list = domains[domain]; - else - { - list = Object.create(null); - domains[domain] = list; - } - list[filter.selector] = key; - hasFilters = true; - } + if (!filter.selector) + continue; - if (!hasFilters) - throw Cr.NS_ERROR_NOT_AVAILABLE; + let domain = filter.selectorDomain || ""; - function escapeChar(match) - { - return "\\" + match.charCodeAt(0).toString(16) + " "; + if (!domains.has(domain)) + domains.set(domain, new Map()); + domains.get(domain).set(filter.selector, key); } - // Return CSS data - let cssTemplate = "-moz-binding: url(about:" + AboutHandler.aboutPrefix + "?%ID%#dummy) !important;"; - for (let domain in domains) - { - let rules = []; - let list = domains[domain]; - - if (domain) - yield ('@-moz-document domain("' + domain.split(",").join('"),domain("') + '"){').replace(/[^\x01-\x7F]/g, escapeChar); - else - { - // Only allow unqualified rules on a few protocols to prevent them from blocking chrome - yield '@-moz-document url-prefix("http://"),url-prefix("https://"),' - + 'url-prefix("mailbox://"),url-prefix("imap://"),' - + 'url-prefix("news://"),url-prefix("snews://"){'; - } + return domains; + }, - for (let selector in list) - yield selector.replace(/[^\x01-\x7F]/g, escapeChar) + "{" + cssTemplate.replace("%ID%", list[selector]) + "}"; - yield '}'; - } + /** + * Returns a list of selectors that apply on each website unconditionally. + * @returns {string[]} + */ + getUnconditionalSelectors() + { + if (!unconditionalSelectors) + unconditionalSelectors = Object.keys(filterKeyBySelector); + return unconditionalSelectors.slice(); }, /** - * Unapplies current stylesheet URL + * Returns a list of filter keys for selectors which apply to all websites + * without exception. + * @returns {number[]} */ - unapply: function() + getUnconditionalFilterKeys() { - if (ElemHide.applied) + if (!unconditionalFilterKeys) { - try - { - Utils.styleService.unregisterSheet(styleURL, Ci.nsIStyleSheetService.USER_SHEET); - } - catch (e) - { - Cu.reportError(e); - } - ElemHide.applied = false; + let selectors = this.getUnconditionalSelectors(); + unconditionalFilterKeys = []; + for (let selector of selectors) + unconditionalFilterKeys.push(filterKeyBySelector[selector]); } + return unconditionalFilterKeys.slice(); }, + /** - * Retrieves the currently applied stylesheet URL - * @type String + * Constant used by getSelectorsForDomain to return all selectors applying to + * a particular hostname. */ - get styleURL() - { - return ElemHide.applied ? styleURL.spec : null; - }, + ALL_MATCHING: 0, /** - * Retrieves an element hiding filter by the corresponding protocol key + * Constant used by getSelectorsForDomain to exclude selectors which apply to + * all websites without exception. */ - getFilterByKey: function(/**String*/ key) /**Filter*/ - { - return (key in filterByKey ? filterByKey[key] : null); - }, + NO_UNCONDITIONAL: 1, /** - * Returns a list of all selectors active on a particular domain (currently - * used only in Chrome, Opera and Safari). + * Constant used by getSelectorsForDomain to return only selectors for filters + * which specifically match the given host name. */ - getSelectorsForDomain: function(/**String*/ domain, /**Boolean*/ specificOnly) + SPECIFIC_ONLY: 2, + + /** + * Determines from the current filter list which selectors should be applied + * on a particular host name. Optionally returns the corresponding filter + * keys. + * @param {string} domain + * @param {number} [criteria] + * One of the following: ElemHide.ALL_MATCHING, ElemHide.NO_UNCONDITIONAL or + * ElemHide.SPECIFIC_ONLY. + * @param {boolean} [provideFilterKeys] + * If true, the function will return a list of corresponding filter keys in + * addition to selectors. + * @returns {string[]|Array.<string[]>} + * List of selectors or an array with two elements (list of selectors and + * list of corresponding keys) if provideFilterKeys is true. + */ + getSelectorsForDomain(domain, criteria, provideFilterKeys) { - let result = []; - for (let key in filterByKey) + let filterKeys = []; + let selectors = []; + + if (typeof criteria == "undefined") + criteria = ElemHide.ALL_MATCHING; + if (criteria < ElemHide.NO_UNCONDITIONAL) { - let filter = filterByKey[key]; - if (specificOnly && (!filter.domains || filter.domains[""])) - continue; + selectors = this.getUnconditionalSelectors(); + if (provideFilterKeys) + filterKeys = this.getUnconditionalFilterKeys(); + } + + let specificOnly = (criteria >= ElemHide.SPECIFIC_ONLY); + let seenFilters = Object.create(null); + let currentDomain = domain ? domain.toUpperCase() : ""; + while (true) + { + if (specificOnly && currentDomain == "") + break; - if (filter.isActiveOnDomain(domain) && !this.getException(filter, domain)) - result.push(filter.selector); + let filters = filtersByDomain[currentDomain]; + if (filters) + { + for (let filterKey in filters) + { + if (filterKey in seenFilters) + continue; + seenFilters[filterKey] = true; + + let filter = filters[filterKey]; + if (filter && !this.getException(filter, domain)) + { + selectors.push(filter.selector); + // It is faster to always push the key, even if not required. + filterKeys.push(filterKey); + } + } + } + + if (currentDomain == "") + break; + + let nextDot = currentDomain.indexOf("."); + currentDomain = nextDot == -1 ? "" : currentDomain.substr(nextDot + 1); } - return result; + + if (provideFilterKeys) + return [selectors, filterKeys]; + return selectors; } }; |