Index: composer.html
===================================================================
--- a/composer.html
+++ b/composer.html
@@ -19,16 +19,17 @@
 
 <html>
 <head>
 <meta charset="utf-8">
 <title>Block element</title>
 <link type="text/css" href="jquery-ui/css/smoothness/jquery-ui-1.12.1.custom.css" rel="stylesheet" />
 <script type="text/javascript" src="jquery-ui/js/jquery-3.2.1.min.js"></script>
 <script type="text/javascript" src="jquery-ui/js/jquery-ui-1.12.1.custom.min.js"></script>
+<script type="text/javascript" src="base.js"></script>
 <script type="text/javascript" src="polyfill.js"></script>
 <script type="text/javascript" src="ext/common.js"></script>
 <script type="text/javascript" src="ext/content.js"></script>
 <script type="text/javascript" src="i18n.js" charset="utf-8"></script>
 <script type="text/javascript" src="composer.js" charset="utf-8"></script>
 <style type="text/css">
   *
   {
Index: desktop-options.html
===================================================================
--- a/desktop-options.html
+++ b/desktop-options.html
@@ -18,16 +18,17 @@
   -->
 
 <html>
 <head>
 <meta charset="utf-8">
 <link type="text/css" href="jquery-ui/css/smoothness/jquery-ui-1.12.1.custom.css" rel="stylesheet" />
 <script type="text/javascript" src="jquery-ui/js/jquery-3.2.1.min.js"></script>
 <script type="text/javascript" src="jquery-ui/js/jquery-ui-1.12.1.custom.min.js"></script>
+<script type="text/javascript" src="base.js"></script>
 <script type="text/javascript" src="polyfill.js"></script>
 <script type="text/javascript" src="ext/common.js"></script>
 <script type="text/javascript" src="ext/content.js"></script>
 <script type="text/javascript" src="i18n.js" charset="utf-8"></script>
 <script type="text/javascript" src="desktop-options.js" charset="utf-8"></script>
 <title>Adblock Plus Options</title>
 <style type="text/css" media="screen">
 body
Index: ext/background.js
===================================================================
--- a/ext/background.js
+++ b/ext/background.js
@@ -10,84 +10,93 @@
  * 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);
+    [internal]: {
+      delete(pageId)
+      {
+        this[internal].map.delete(pageId);
 
-      if (this._map.size == 0)
-        nonEmptyPageMaps.delete(this);
+        if (this[internal].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);
+      this[internal].delete(page.id);
     }
   };
 
-  ext._removeFromAllPageMaps = pageId =>
+  ext[internal].removeFromAllPageMaps = pageId =>
   {
     for (let pageMap of nonEmptyPageMaps)
-      pageMap._delete(pageId);
+      pageMap[internal].delete(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 +124,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.pages.onLoading[internal].dispatch(new Page(tab));
   });
 
   function createFrame(tabId, frameId)
   {
     let frames = framesOfTabs.get(tabId);
     if (!frames)
     {
       frames = new Map();
@@ -151,28 +160,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.pages.onLoading[internal].dispatch(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,156 +281,166 @@
     {
       updatePageFrameStructure(details.frameId, details.tabId, url,
                                details.parentFrameId);
     }
   });
 
   function forgetTab(tabId)
   {
-    ext.pages.onRemoved._dispatch(tabId);
+    ext.pages.onRemoved[internal].dispatch(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.pages.onActivated[internal].dispatch(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)
+    [internal]: {
+      applyChanges()
       {
-        // 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)
+        if ("iconPath" in this[internal].changes)
         {
-          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
+          // 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)
           {
-            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});
+            let path = {
+              16: this[internal].changes.iconPath.replace("$size", "16"),
+              19: this[internal].changes.iconPath.replace("$size", "19"),
+              20: this[internal].changes.iconPath.replace("$size", "20"),
+              32: this[internal].changes.iconPath.replace("$size", "32"),
+              38: this[internal].changes.iconPath.replace("$size", "38"),
+              40: this[internal].changes.iconPath.replace("$size", "40")
+            };
+            try
+            {
+              browser.browserAction.setIcon({
+                tabId: this[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: this[internal].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)
+        if ("badgeText" in this[internal].changes)
         {
-          browser.browserAction.setBadgeText({
-            tabId: this._tabId,
-            text: this._changes.badgeText
-          });
+          // 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[internal].tabId,
+              text: this[internal].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)
+        if ("badgeColor" in this[internal].changes)
         {
-          browser.browserAction.setBadgeBackgroundColor({
-            tabId: this._tabId,
-            color: this._changes.badgeColor
-          });
+          // 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[internal].tabId,
+              color: this[internal].changes.badgeColor
+            });
+          }
         }
-      }
 
-      this._changes = null;
-    },
-    _queueChanges()
-    {
-      browser.tabs.get(this._tabId, () =>
+        this[internal].changes = null;
+      },
+      queueChanges()
       {
-        // 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)
+        browser.tabs.get(this[internal].tabId, () =>
         {
-          let onReplaced = (addedTabId, removedTabId) =>
+          // 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)
           {
-            if (addedTabId == this._tabId)
+            let onReplaced = (addedTabId, removedTabId) =>
             {
-              browser.tabs.onReplaced.removeListener(onReplaced);
-              this._applyChanges();
-            }
-          };
-          browser.tabs.onReplaced.addListener(onReplaced);
-        }
-        else
+              if (addedTabId == this[internal].tabId)
+              {
+                browser.tabs.onReplaced.removeListener(onReplaced);
+                this[internal].applyChanges();
+              }
+            };
+            browser.tabs.onReplaced.addListener(onReplaced);
+          }
+          else
+          {
+            this[internal].applyChanges();
+          }
+        });
+      },
+      addChange(name, value)
+      {
+        if (!this[internal].changes)
         {
-          this._applyChanges();
+          this[internal].changes = {};
+          this[internal].queueChanges();
         }
-      });
-    },
-    _addChange(name, value)
-    {
-      if (!this._changes)
-      {
-        this._changes = {};
-        this._queueChanges();
+
+        this[internal].changes[name] = value;
       }
-
-      this._changes[name] = value;
     },
     setIcon(path)
     {
-      this._addChange("iconPath", path);
+      this[internal].addChange("iconPath", path);
     },
     setBadge(badge)
     {
       if (!badge)
       {
-        this._addChange("badgeText", "");
+        this[internal].addChange("badgeText", "");
       }
       else
       {
         if ("number" in badge)
-          this._addChange("badgeText", badge.number.toString());
+          this[internal].addChange("badgeText", badge.number.toString());
 
         if ("color" in badge)
-          this._addChange("badgeColor", badge.color);
+          this[internal].addChange("badgeColor", badge.color);
       }
     }
   };
 
 
   /* Context menus */
 
   let contextMenuItems = new ext.PageMap();
@@ -462,31 +481,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 +553,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 +638,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.webRequest.onBeforeRequest[internal]
+        .dispatch(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 +678,17 @@
           if (frame)
             return frame.parent || null;
 
           return frames.get(0) || null;
         }
       };
     }
 
-    return ext.onMessage._dispatch(
+    return ext.onMessage[internal].dispatch(
       message, sender, sendResponse
     ).includes(true);
   });
 
 
   /* Storage */
 
   ext.storage = {
Index: ext/common.js
===================================================================
--- a/ext/common.js
+++ b/ext/common.js
@@ -10,42 +10,47 @@
  * 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 defineNamespace */
+
 "use strict";
 
+const internal = Symbol();
+
 {
   window.ext = {};
 
-  let EventTarget = ext._EventTarget = function()
+  defineNamespace(ext, internal);
+
+  let EventTarget = ext[internal].EventTarget = function()
   {
-    this._listeners = new Set();
+    defineNamespace(this, internal);
+
+    this[internal].listeners = new Set();
   };
   EventTarget.prototype = {
+    [internal]: {
+      dispatch(...args)
+      {
+        return [...this[internal].listeners].map(listener => listener(...args));
+      }
+    },
     addListener(listener)
     {
-      this._listeners.add(listener);
+      this[internal].listeners.add(listener);
     },
     removeListener(listener)
     {
-      this._listeners.delete(listener);
-    },
-    _dispatch(...args)
-    {
-      let results = [];
-
-      for (let listener of this._listeners)
-        results.push(listener(...args));
-
-      return results;
+      this[internal].listeners.delete(listener);
     }
   };
 
 
   /* Message passing */
 
-  ext.onMessage = new ext._EventTarget();
+  ext.onMessage = new ext[internal].EventTarget();
 }
Index: ext/content.js
===================================================================
--- a/ext/content.js
+++ b/ext/content.js
@@ -1,22 +1,26 @@
+/* global internal */
+
 "use strict";
 
 // Firefox 55 erroneously sends messages from the content script to the
 // devtools panel:
 // https://bugzilla.mozilla.org/show_bug.cgi?id=1383310
 // As a workaround, listen for messages only if this isn't the devtools panel.
 // Note that Firefox processes API access lazily, so browser.devtools will
 // always exist but will have undefined as its value on other pages.
 if (!browser.devtools)
 {
   // Listen for messages from the background page.
   browser.runtime.onMessage.addListener((message, sender, sendResponse) =>
   {
-    return ext.onMessage._dispatch(message, {}, sendResponse).includes(true);
+    return ext.onMessage[internal].dispatch(
+      message, {}, sendResponse
+    ).includes(true);
   });
 }
 
 {
   let port = null;
 
   ext.onExtensionUnloaded = {
     addListener(listener)
Index: lib/requestBlocker.js
===================================================================
--- a/lib/requestBlocker.js
+++ b/lib/requestBlocker.js
@@ -86,25 +86,24 @@
     devtools.logRequest(
       page, url, type, docDomain,
       thirdParty, sitekey,
       specificOnly, filter
     );
   }
 }
 
-ext.webRequest.onBeforeRequest.addListener((url, type, page, frame) =>
+function shouldAllowRequest(url, type, page, frame)
 {
   let docDomain = null;
   let sitekey = null;
   let specificOnly = false;
   let thirdParty = false;
   let urlString = stringifyURL(url);
 
-
   if (frame && page)
   {
     if (checkWhitelisted(page, frame))
       return true;
 
     docDomain = extractHostFromFrame(frame);
     sitekey = getKey(page, frame);
     thirdParty = isThirdParty(url, docDomain);
@@ -120,17 +119,19 @@
   );
 
   setTimeout(onBeforeRequestAsync, 0, page, urlString,
                                       mappedType, docDomain,
                                       thirdParty, sitekey,
                                       specificOnly, filter);
 
   return !(filter instanceof BlockingFilter);
-});
+}
+
+ext.webRequest.onBeforeRequest.addListener(shouldAllowRequest);
 
 port.on("filters.collapse", (message, sender) =>
 {
   if (checkWhitelisted(sender.page, sender.frame))
     return false;
 
   let typeMask = RegExpFilter.typeMap[message.mediatype];
   let documentHost = extractHostFromFrame(sender.frame);
@@ -213,15 +214,11 @@
   // Edge supports neither webRequest.ResourceType nor WebSocket blocking yet:
   // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/10297376/
   if (browser.webRequest.ResourceType &&
       (msg.requestType.toUpperCase() in browser.webRequest.ResourceType))
   {
     return false;
   }
 
-  return ext.webRequest.onBeforeRequest._dispatch(
-     new URL(msg.url),
-     msg.requestType,
-     sender.page,
-     sender.frame
-  ).includes(false);
+  return !shouldAllowRequest(new URL(msg.url), msg.requestType, sender.page,
+                             sender.frame);
 });
Index: metadata.chrome
===================================================================
--- a/metadata.chrome
+++ b/metadata.chrome
@@ -6,23 +6,25 @@
   <all_urls>
   contextMenus
   webRequest
   webRequestBlocking
   webNavigation
   storage
   unlimitedStorage
   notifications
-backgroundScripts = polyfill.js
+backgroundScripts = base.js
+  polyfill.js
   ext/common.js
   ext/background.js
   lib/compat.js
   lib/publicSuffixList.js
   lib/adblockplus.js
-testScripts = ../polyfill.js
+testScripts = ../base.js
+  ../polyfill.js
   ../ext/common.js
   ../ext/background.js
   ../lib/compat.js
   ../lib/publicSuffixList.js
   tests.js
 options = options.html
 devtools = devtools.html
 browserAction = icons/abp-16.png icons/abp-19.png icons/abp-20.png icons/abp-32.png icons/abp-38.png icons/abp-40.png popup.html
@@ -30,17 +32,21 @@
   icons/detailed/abp-64.png icons/detailed/abp-128.png
 managedStorageSchema = managed-storage-schema.json
 
 [compat]
 chrome = 49.0
 opera = 36.0
 
 [contentScripts]
-document_start = polyfill.js ext/common.js ext/content.js include.preload.js
+document_start = base.js
+  polyfill.js
+  ext/common.js
+  ext/content.js
+  include.preload.js
 document_end = include.postload.js
 
 [mapping]
 subscriptions.xml = adblockpluscore/chrome/content/ui/subscriptions.xml
 firstRun.html = adblockplusui/firstRun.html
 common.js = adblockplusui/common.js
 firstRun.js = adblockplusui/firstRun.js
 i18n.js = adblockplusui/i18n.js
Index: popup.html
===================================================================
--- a/popup.html
+++ b/popup.html
@@ -16,16 +16,17 @@
   - You should have received a copy of the GNU General Public License
   - along with Adblock Plus.  If not, see <http://www.gnu.org/licenses/>.
   -->
 
 <html>
 <head>
   <meta charset="utf-8">
   <link rel="stylesheet" type="text/css" href="skin/popup.css">
+  <script src="base.js"></script>
   <script src="polyfill.js"></script>
   <script src="ext/common.js"></script>
   <script src="i18n.js"></script>
   <script src="popup.js"></script>
 </head>
 <!-- Set tabindex to work around Chromium issue 304532 -->
 <body class="nohtml" tabindex="1">
 
