| Index: ext/background.js | 
| =================================================================== | 
| --- a/ext/background.js | 
| +++ b/ext/background.js | 
| @@ -10,84 +10,92 @@ | 
| * 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 Adblock Plus.  If not, see <http://www.gnu.org/licenses/>. | 
| */ | 
|  | 
| +/* global internal, defineNamespace */ | 
| + | 
| "use strict"; | 
|  | 
| { | 
| let nonEmptyPageMaps = new Set(); | 
|  | 
| let PageMap = ext.PageMap = function() | 
| { | 
| -    this._map = new Map(); | 
| +    defineNamespace(this, internal); | 
| + | 
| +    this[internal].map = new Map(); | 
| }; | 
| PageMap.prototype = { | 
| -    _delete(id) | 
| -    { | 
| -      this._map.delete(id); | 
| - | 
| -      if (this._map.size == 0) | 
| -        nonEmptyPageMaps.delete(this); | 
| -    }, | 
| keys() | 
| { | 
| -      return Array.from(this._map.keys()).map(ext.getPage); | 
| +      return Array.from(this[internal].map.keys()).map(ext.getPage); | 
| }, | 
| get(page) | 
| { | 
| -      return this._map.get(page.id); | 
| +      return this[internal].map.get(page.id); | 
| }, | 
| set(page, value) | 
| { | 
| -      this._map.set(page.id, value); | 
| +      this[internal].map.set(page.id, value); | 
| nonEmptyPageMaps.add(this); | 
| }, | 
| has(page) | 
| { | 
| -      return this._map.has(page.id); | 
| +      return this[internal].map.has(page.id); | 
| }, | 
| clear() | 
| { | 
| -      this._map.clear(); | 
| +      this[internal].map.clear(); | 
| nonEmptyPageMaps.delete(this); | 
| }, | 
| delete(page) | 
| { | 
| -      this._delete(page.id); | 
| +      deletePage(this, page.id); | 
| } | 
| }; | 
|  | 
| -  ext._removeFromAllPageMaps = pageId => | 
| +  function deletePage(pageMap, pageId) | 
| +  { | 
| +    pageMap[internal].map.delete(pageId); | 
| + | 
| +    if (pageMap[internal].map.size == 0) | 
| +      nonEmptyPageMaps.delete(pageMap); | 
| +  } | 
| + | 
| +  ext[internal].removeFromAllPageMaps = pageId => | 
| { | 
| for (let pageMap of nonEmptyPageMaps) | 
| -      pageMap._delete(pageId); | 
| +      deletePage(pageMap, pageId); | 
| }; | 
|  | 
| /* Pages */ | 
|  | 
| let Page = ext.Page = function(tab) | 
| { | 
| +    defineNamespace(this, internal); | 
| + | 
| this.id = tab.id; | 
| -    this._url = tab.url && new URL(tab.url); | 
| + | 
| +    this[internal].url = tab.url && new URL(tab.url); | 
|  | 
| this.browserAction = new BrowserAction(tab.id); | 
| this.contextMenus = new ContextMenus(this); | 
| }; | 
| Page.prototype = { | 
| get url() | 
| { | 
| // usually our Page objects are created from Chrome's Tab objects, which | 
| // provide the url. So we can return the url given in the constructor. | 
| -      if (this._url) | 
| -        return this._url; | 
| +      if (this[internal].url) | 
| +        return this[internal].url; | 
|  | 
| // but sometimes we only have the tab id when we create a Page object. | 
| // In that case we get the url from top frame of the tab, recorded by | 
| // the onBeforeRequest handler. | 
| let frames = framesOfTabs.get(this.id); | 
| if (frames) | 
| { | 
| let frame = frames.get(0); | 
| @@ -115,25 +123,25 @@ | 
| callback(new Page(openedTab)); | 
| } | 
| }; | 
| browser.tabs.onUpdated.addListener(onUpdated); | 
| }; | 
| } | 
|  | 
| ext.pages = { | 
| -    onLoading: new ext._EventTarget(), | 
| -    onActivated: new ext._EventTarget(), | 
| -    onRemoved: new ext._EventTarget() | 
| +    onLoading: new ext[internal].EventTarget(), | 
| +    onActivated: new ext[internal].EventTarget(), | 
| +    onRemoved: new ext[internal].EventTarget() | 
| }; | 
|  | 
| browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => | 
| { | 
| if (changeInfo.status == "loading") | 
| -      ext.pages.onLoading._dispatch(new Page(tab)); | 
| +      ext[internal].dispatchEvent(ext.pages.onLoading, new Page(tab)); | 
| }); | 
|  | 
| function createFrame(tabId, frameId) | 
| { | 
| let frames = framesOfTabs.get(tabId); | 
| if (!frames) | 
| { | 
| frames = new Map(); | 
| @@ -151,28 +159,28 @@ | 
| } | 
|  | 
| function updatePageFrameStructure(frameId, tabId, url, parentFrameId) | 
| { | 
| if (frameId == 0) | 
| { | 
| let page = new Page({id: tabId, url}); | 
|  | 
| -      ext._removeFromAllPageMaps(tabId); | 
| +      ext[internal].removeFromAllPageMaps(tabId); | 
|  | 
| browser.tabs.get(tabId, () => | 
| { | 
| // If the tab is prerendered, browser.tabs.get() sets | 
| // browser.runtime.lastError and we have to dispatch the onLoading | 
| // event, since the onUpdated event isn't dispatched for prerendered | 
| // tabs. However, we have to keep relying on the onUpdated event for | 
| // tabs that are already visible. Otherwise browser action changes get | 
| // overridden when Chrome automatically resets them on navigation. | 
| if (browser.runtime.lastError) | 
| -          ext.pages.onLoading._dispatch(page); | 
| +          ext[internal].dispatchEvent(ext.pages.onLoading, page); | 
| }); | 
| } | 
|  | 
| // Update frame URL and parent in frame structure | 
| let frame = createFrame(tabId, frameId); | 
| frame.url = new URL(url); | 
|  | 
| let parentFrame = framesOfTabs.get(tabId).get(parentFrameId); | 
| @@ -272,160 +280,172 @@ | 
| { | 
| updatePageFrameStructure(details.frameId, details.tabId, url, | 
| details.parentFrameId); | 
| } | 
| }); | 
|  | 
| function forgetTab(tabId) | 
| { | 
| -    ext.pages.onRemoved._dispatch(tabId); | 
| +    ext[internal].dispatchEvent(ext.pages.onRemoved, tabId); | 
|  | 
| -    ext._removeFromAllPageMaps(tabId); | 
| +    ext[internal].removeFromAllPageMaps(tabId); | 
| framesOfTabs.delete(tabId); | 
| } | 
|  | 
| browser.tabs.onReplaced.addListener((addedTabId, removedTabId) => | 
| { | 
| forgetTab(removedTabId); | 
| }); | 
|  | 
| browser.tabs.onRemoved.addListener(forgetTab); | 
|  | 
| browser.tabs.onActivated.addListener(details => | 
| { | 
| -    ext.pages.onActivated._dispatch(new Page({id: details.tabId})); | 
| +    ext[internal].dispatchEvent(ext.pages.onActivated, | 
| +                                new Page({id: details.tabId})); | 
| }); | 
|  | 
|  | 
| /* Browser actions */ | 
|  | 
| let BrowserAction = function(tabId) | 
| { | 
| -    this._tabId = tabId; | 
| -    this._changes = null; | 
| +    defineNamespace(this, internal); | 
| + | 
| +    this[internal].tabId = tabId; | 
| +    this[internal].changes = null; | 
| }; | 
| BrowserAction.prototype = { | 
| -    _applyChanges() | 
| -    { | 
| -      if ("iconPath" in this._changes) | 
| -      { | 
| -        // Firefox for Android displays the browser action not as an icon but | 
| -        // as a menu item. There is no icon, but such an option may be added in | 
| -        // the future. | 
| -        // https://bugzilla.mozilla.org/show_bug.cgi?id=1331746 | 
| -        if ("setIcon" in browser.browserAction) | 
| -        { | 
| -          let path = { | 
| -            16: this._changes.iconPath.replace("$size", "16"), | 
| -            19: this._changes.iconPath.replace("$size", "19"), | 
| -            20: this._changes.iconPath.replace("$size", "20"), | 
| -            32: this._changes.iconPath.replace("$size", "32"), | 
| -            38: this._changes.iconPath.replace("$size", "38"), | 
| -            40: this._changes.iconPath.replace("$size", "40") | 
| -          }; | 
| -          try | 
| -          { | 
| -            browser.browserAction.setIcon({tabId: this._tabId, path}); | 
| -          } | 
| -          catch (e) | 
| -          { | 
| -            // Edge throws if passed icon sizes different than 19,20,38,40px. | 
| -            delete path[16]; | 
| -            delete path[32]; | 
| -            browser.browserAction.setIcon({tabId: this._tabId, path}); | 
| -          } | 
| -        } | 
| -      } | 
| - | 
| -      if ("badgeText" in this._changes) | 
| -      { | 
| -        // There is no badge on Firefox for Android; the browser action is | 
| -        // simply a menu item. | 
| -        if ("setBadgeText" in browser.browserAction) | 
| -        { | 
| -          browser.browserAction.setBadgeText({ | 
| -            tabId: this._tabId, | 
| -            text: this._changes.badgeText | 
| -          }); | 
| -        } | 
| -      } | 
| - | 
| -      if ("badgeColor" in this._changes) | 
| -      { | 
| -        // There is no badge on Firefox for Android; the browser action is | 
| -        // simply a menu item. | 
| -        if ("setBadgeBackgroundColor" in browser.browserAction) | 
| -        { | 
| -          browser.browserAction.setBadgeBackgroundColor({ | 
| -            tabId: this._tabId, | 
| -            color: this._changes.badgeColor | 
| -          }); | 
| -        } | 
| -      } | 
| - | 
| -      this._changes = null; | 
| -    }, | 
| -    _queueChanges() | 
| -    { | 
| -      browser.tabs.get(this._tabId, () => | 
| -      { | 
| -        // If the tab is prerendered, browser.tabs.get() sets | 
| -        // browser.runtime.lastError and we have to delay our changes | 
| -        // until the currently visible tab is replaced with the | 
| -        // prerendered tab. Otherwise browser.browserAction.set* fails. | 
| -        if (browser.runtime.lastError) | 
| -        { | 
| -          let onReplaced = (addedTabId, removedTabId) => | 
| -          { | 
| -            if (addedTabId == this._tabId) | 
| -            { | 
| -              browser.tabs.onReplaced.removeListener(onReplaced); | 
| -              this._applyChanges(); | 
| -            } | 
| -          }; | 
| -          browser.tabs.onReplaced.addListener(onReplaced); | 
| -        } | 
| -        else | 
| -        { | 
| -          this._applyChanges(); | 
| -        } | 
| -      }); | 
| -    }, | 
| -    _addChange(name, value) | 
| -    { | 
| -      if (!this._changes) | 
| -      { | 
| -        this._changes = {}; | 
| -        this._queueChanges(); | 
| -      } | 
| - | 
| -      this._changes[name] = value; | 
| -    }, | 
| setIcon(path) | 
| { | 
| -      this._addChange("iconPath", path); | 
| +      addBrowserActionChange(this, "iconPath", path); | 
| }, | 
| setBadge(badge) | 
| { | 
| if (!badge) | 
| { | 
| -        this._addChange("badgeText", ""); | 
| +        addBrowserActionChange(this, "badgeText", ""); | 
| } | 
| else | 
| { | 
| if ("number" in badge) | 
| -          this._addChange("badgeText", badge.number.toString()); | 
| +          addBrowserActionChange(this, "badgeText", badge.number.toString()); | 
|  | 
| if ("color" in badge) | 
| -          this._addChange("badgeColor", badge.color); | 
| +          addBrowserActionChange(this, "badgeColor", badge.color); | 
| } | 
| } | 
| }; | 
|  | 
| +  function queueBrowserActionChanges(browserAction) | 
| +  { | 
| +    browser.tabs.get(browserAction[internal].tabId, () => | 
| +    { | 
| +      // If the tab is prerendered, browser.tabs.get() sets | 
| +      // browser.runtime.lastError and we have to delay our changes | 
| +      // until the currently visible tab is replaced with the | 
| +      // prerendered tab. Otherwise browser.browserAction.set* fails. | 
| +      if (browser.runtime.lastError) | 
| +      { | 
| +        let onReplaced = (addedTabId, removedTabId) => | 
| +        { | 
| +          if (addedTabId == browserAction[internal].tabId) | 
| +          { | 
| +            browser.tabs.onReplaced.removeListener(onReplaced); | 
| +            applyBrowserActionChanges(browserAction); | 
| +          } | 
| +        }; | 
| +        browser.tabs.onReplaced.addListener(onReplaced); | 
| +      } | 
| +      else | 
| +      { | 
| +        applyBrowserActionChanges(browserAction); | 
| +      } | 
| +    }); | 
| +  } | 
| + | 
| +  function applyBrowserActionChanges(browserAction) | 
| +  { | 
| +    if ("iconPath" in browserAction[internal].changes) | 
| +    { | 
| +      // Firefox for Android displays the browser action not as an icon but | 
| +      // as a menu item. There is no icon, but such an option may be added in | 
| +      // the future. | 
| +      // https://bugzilla.mozilla.org/show_bug.cgi?id=1331746 | 
| +      if ("setIcon" in browser.browserAction) | 
| +      { | 
| +        let path = { | 
| +          16: browserAction[internal].changes.iconPath.replace("$size", "16"), | 
| +          19: browserAction[internal].changes.iconPath.replace("$size", "19"), | 
| +          20: browserAction[internal].changes.iconPath.replace("$size", "20"), | 
| +          32: browserAction[internal].changes.iconPath.replace("$size", "32"), | 
| +          38: browserAction[internal].changes.iconPath.replace("$size", "38"), | 
| +          40: browserAction[internal].changes.iconPath.replace("$size", "40") | 
| +        }; | 
| +        try | 
| +        { | 
| +          browser.browserAction.setIcon({ | 
| +            tabId: browserAction[internal].tabId, | 
| +            path | 
| +          }); | 
| +        } | 
| +        catch (e) | 
| +        { | 
| +          // Edge throws if passed icon sizes different than 19,20,38,40px. | 
| +          delete path[16]; | 
| +          delete path[32]; | 
| +          browser.browserAction.setIcon({ | 
| +            tabId: browserAction[internal].tabId, | 
| +            path | 
| +          }); | 
| +        } | 
| +      } | 
| +    } | 
| + | 
| +    if ("badgeText" in browserAction[internal].changes) | 
| +    { | 
| +      // There is no badge on Firefox for Android; the browser action is | 
| +      // simply a menu item. | 
| +      if ("setBadgeText" in browser.browserAction) | 
| +      { | 
| +        browser.browserAction.setBadgeText({ | 
| +          tabId: browserAction[internal].tabId, | 
| +          text: browserAction[internal].changes.badgeText | 
| +        }); | 
| +      } | 
| +    } | 
| + | 
| +    if ("badgeColor" in browserAction[internal].changes) | 
| +    { | 
| +      // There is no badge on Firefox for Android; the browser action is | 
| +      // simply a menu item. | 
| +      if ("setBadgeBackgroundColor" in browser.browserAction) | 
| +      { | 
| +        browser.browserAction.setBadgeBackgroundColor({ | 
| +          tabId: browserAction[internal].tabId, | 
| +          color: browserAction[internal].changes.badgeColor | 
| +        }); | 
| +      } | 
| +    } | 
| + | 
| +    browserAction[internal].changes = null; | 
| +  } | 
| + | 
| +  function addBrowserActionChange(browserAction, name, value) | 
| +  { | 
| +    if (!browserAction[internal].changes) | 
| +    { | 
| +      browserAction[internal].changes = {}; | 
| +      queueBrowserActionChanges(browserAction); | 
| +    } | 
| + | 
| +    browserAction[internal].changes[name] = value; | 
| +  } | 
| + | 
|  | 
| /* Context menus */ | 
|  | 
| let contextMenuItems = new ext.PageMap(); | 
| let contextMenuUpdating = false; | 
|  | 
| let updateContextMenu = () => | 
| { | 
| @@ -462,31 +482,33 @@ | 
| }); | 
| }); | 
| }); | 
| }); | 
| }; | 
|  | 
| let ContextMenus = function(page) | 
| { | 
| -    this._page = page; | 
| +    defineNamespace(this, internal); | 
| + | 
| +    this[internal].page = page; | 
| }; | 
| ContextMenus.prototype = { | 
| create(item) | 
| { | 
| -      let items = contextMenuItems.get(this._page); | 
| +      let items = contextMenuItems.get(this[internal].page); | 
| if (!items) | 
| -        contextMenuItems.set(this._page, items = []); | 
| +        contextMenuItems.set(this[internal].page, items = []); | 
|  | 
| items.push(item); | 
| updateContextMenu(); | 
| }, | 
| remove(item) | 
| { | 
| -      let items = contextMenuItems.get(this._page); | 
| +      let items = contextMenuItems.get(this[internal].page); | 
| if (items) | 
| { | 
| let index = items.indexOf(item); | 
| if (index != -1) | 
| { | 
| items.splice(index, 1); | 
| updateContextMenu(); | 
| } | 
| @@ -532,17 +554,17 @@ | 
| browser.webRequest.handlerBehaviorChanged(); | 
|  | 
| handlerBehaviorChangedQuota--; | 
| setTimeout(() => { handlerBehaviorChangedQuota++; }, 600000); | 
| } | 
| } | 
|  | 
| ext.webRequest = { | 
| -    onBeforeRequest: new ext._EventTarget(), | 
| +    onBeforeRequest: new ext[internal].EventTarget(), | 
| handlerBehaviorChanged() | 
| { | 
| // Defer handlerBehaviorChanged() until navigation occurs. | 
| // There wouldn't be any visible effect when calling it earlier, | 
| // but it's an expensive operation and that way we avoid to call | 
| // it multiple times, if multiple filters are added/removed. | 
| let {onBeforeNavigate} = browser.webNavigation; | 
| if (!onBeforeNavigate.hasListener(propagateHandlerBehaviorChange)) | 
| @@ -617,19 +639,21 @@ | 
| let frame = null; | 
| let page = null; | 
| if (details.tabId != -1) | 
| { | 
| frame = ext.getFrame(details.tabId, frameId); | 
| page = new Page({id: details.tabId}); | 
| } | 
|  | 
| -    if (ext.webRequest.onBeforeRequest._dispatch( | 
| -        url, type, page, frame).includes(false)) | 
| +    if (ext[internal].dispatchEvent(ext.webRequest.onBeforeRequest, | 
| +                                    url, type, page, frame).includes(false)) | 
| +    { | 
| return {cancel: true}; | 
| +    } | 
| }, {urls: ["<all_urls>"]}, ["blocking"]); | 
|  | 
|  | 
| /* Message passing */ | 
|  | 
| browser.runtime.onMessage.addListener((message, rawSender, sendResponse) => | 
| { | 
| let sender = {}; | 
| @@ -655,17 +679,18 @@ | 
| if (frame) | 
| return frame.parent || null; | 
|  | 
| return frames.get(0) || null; | 
| } | 
| }; | 
| } | 
|  | 
| -    return ext.onMessage._dispatch( | 
| +    return ext[internal].dispatchEvent( | 
| +      ext.onMessage, | 
| message, sender, sendResponse | 
| ).includes(true); | 
| }); | 
|  | 
|  | 
| /* Storage */ | 
|  | 
| ext.storage = { | 
|  |