Bug 1175770 - New extension API (r=Mossop)
☠☠ backed out by 976ba676a659 ☠ ☠
authorBill McCloskey <billm@mozilla.com>
Wed, 03 Jun 2015 15:34:44 -0700
changeset 287458 4e3821b236f93a99a2f415291af3ec14390a318d
parent 287457 b821b18a1bfb9ade72007868f117fc155df9e804
child 287459 6d2e3f71205b0f8bf88cdf21932d2739611e8392
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-beta@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMossop
bugs1175770
milestone42.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1175770 - New extension API (r=Mossop)
browser/base/content/tab-content.js
browser/components/extensions/bootstrap.js
browser/components/extensions/ext-browserAction.js
browser/components/extensions/ext-contextMenus.js
browser/components/extensions/ext-tabs.js
browser/components/extensions/ext-utils.js
browser/components/extensions/ext-windows.js
browser/components/extensions/extension.svg
browser/components/extensions/jar.mn
browser/components/extensions/moz.build
browser/components/extensions/prepare.py
browser/components/moz.build
browser/components/nsBrowserGlue.js
browser/modules/E10SUtils.jsm
browser/themes/linux/browser.css
browser/themes/osx/browser.css
browser/themes/windows/browser.css
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/ExtensionManagement.jsm
toolkit/components/extensions/ExtensionStorage.jsm
toolkit/components/extensions/ExtensionUtils.jsm
toolkit/components/extensions/ext-alarms.js
toolkit/components/extensions/ext-backgroundPage.js
toolkit/components/extensions/ext-extension.js
toolkit/components/extensions/ext-i18n.js
toolkit/components/extensions/ext-idle.js
toolkit/components/extensions/ext-notifications.js
toolkit/components/extensions/ext-runtime.js
toolkit/components/extensions/ext-storage.js
toolkit/components/extensions/ext-webNavigation.js
toolkit/components/extensions/ext-webRequest.js
toolkit/components/extensions/jar.mn
toolkit/components/extensions/moz.build
toolkit/components/moz.build
toolkit/components/utils/simpleServices.js
toolkit/modules/Locale.jsm
toolkit/modules/addons/MatchPattern.jsm
toolkit/modules/moz.build
toolkit/mozapps/extensions/internal/XPIProvider.jsm
--- a/browser/base/content/tab-content.js
+++ b/browser/base/content/tab-content.js
@@ -4,16 +4,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* This content script contains code that requires a tab browser. */
 
 let {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/ExtensionContent.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
   "resource:///modules/E10SUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
   "resource://gre/modules/BrowserUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AboutReader",
@@ -651,8 +652,13 @@ let DOMFullscreenHandler = {
         removeEventListener("MozAfterPaint", this);
         sendAsyncMessage("DOMFullscreen:Painted");
         break;
       }
     }
   }
 };
 DOMFullscreenHandler.init();
+
+ExtensionContent.init(this);
+addEventListener("unload", () => {
+  ExtensionContent.uninit(this);
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/bootstrap.js
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/Extension.jsm");
+
+let extension;
+
+function startup(data, reason)
+{
+  extension = new Extension(data);
+  extension.startup();
+}
+
+function shutdown(data, reason)
+{
+  extension.shutdown();
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-browserAction.js
@@ -0,0 +1,326 @@
+XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
+                                  "resource:///modules/CustomizableUI.jsm");
+
+Cu.import("resource://gre/modules/devtools/event-emitter.js");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+let {
+  EventManager,
+  DefaultWeakMap,
+  ignoreEvent,
+  runSafe,
+} = ExtensionUtils;
+
+// WeakMap[Extension -> BrowserAction]
+let browserActionMap = new WeakMap();
+
+function browserActionOf(extension)
+{
+  return browserActionMap.get(extension);
+}
+
+function makeWidgetId(id)
+{
+  id = id.toLowerCase();
+  return id.replace(/[^a-z0-9_-]/g, "_");
+}
+
+let nextActionId = 0;
+
+// Responsible for the browser_action section of the manifest as well
+// as the associated popup.
+function BrowserAction(options, extension)
+{
+  this.extension = extension;
+  this.id = makeWidgetId(extension.id) + "-browser-action";
+  this.widget = null;
+
+  this.title = new DefaultWeakMap(extension.localize(options.default_title));
+  this.badgeText = new DefaultWeakMap();
+  this.badgeBackgroundColor = new DefaultWeakMap();
+  this.icon = new DefaultWeakMap(options.default_icon);
+  this.popup = new DefaultWeakMap(options.default_popup);
+
+  // Make the default something that won't compare equal to anything.
+  this.prevPopups = new DefaultWeakMap({});
+
+  this.context = null;
+}
+
+BrowserAction.prototype = {
+  build() {
+    let widget = CustomizableUI.createWidget({
+      id: this.id,
+      type: "custom",
+      removable: true,
+      defaultArea: CustomizableUI.AREA_NAVBAR,
+      onBuild: document => {
+        let node = document.createElement("toolbarbutton");
+        node.id = this.id;
+        node.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional badged-button");
+        node.setAttribute("constrain-size", "true");
+
+        this.updateTab(null, node);
+
+        let tabbrowser = document.defaultView.gBrowser;
+        tabbrowser.ownerDocument.addEventListener("TabSelect", () => {
+          this.updateTab(tabbrowser.selectedTab, node);
+        });
+
+        node.addEventListener("command", event => {
+          if (node.getAttribute("type") != "panel") {
+            this.emit("click");
+          }
+        });
+
+        return node;
+      },
+    });
+    this.widget = widget;
+  },
+
+  // Initialize the toolbar icon and popup given that |tab| is the
+  // current tab and |node| is the CustomizableUI node. Note: |tab|
+  // will be null if we don't know the current tab yet (during
+  // initialization).
+  updateTab(tab, node) {
+    let window = node.ownerDocument.defaultView;
+
+    let title = this.getProperty(tab, "title");
+    if (title) {
+      node.setAttribute("tooltiptext", title);
+      node.setAttribute("label", title);
+    } else {
+      node.removeAttribute("tooltiptext");
+      node.removeAttribute("label");
+    }
+
+    let badgeText = this.badgeText.get(tab);
+    if (badgeText) {
+      node.setAttribute("badge", badgeText);
+    } else {
+      node.removeAttribute("badge");
+    }
+
+    function toHex(n) {
+      return Math.floor(n / 16).toString(16) + (n % 16).toString(16);
+    }
+
+    let badgeNode = node.ownerDocument.getAnonymousElementByAttribute(node,
+                                        'class', 'toolbarbutton-badge');
+    if (badgeNode) {
+      let color = this.badgeBackgroundColor.get(tab);
+      if (Array.isArray(color)) {
+        color = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
+      }
+      badgeNode.style.backgroundColor = color;
+    }
+
+    let iconURL = this.getIcon(tab, node);
+    node.setAttribute("image", iconURL);
+
+    let popup = this.getProperty(tab, "popup");
+
+    if (popup != this.prevPopups.get(window)) {
+      this.prevPopups.set(window, popup);
+
+      let panel = node.querySelector("panel");
+      if (panel) {
+        panel.remove();
+      }
+
+      if (popup) {
+        let popupURL = this.extension.baseURI.resolve(popup);
+        node.setAttribute("type", "panel");
+
+        let document = node.ownerDocument;
+        let panel = document.createElement("panel");
+        panel.setAttribute("class", "browser-action-panel");
+        panel.setAttribute("type", "arrow");
+        panel.setAttribute("flip", "slide");
+        node.appendChild(panel);
+
+        let browser = document.createElementNS(XUL_NS, "browser");
+        browser.setAttribute("type", "content");
+        browser.setAttribute("disableglobalhistory", "true");
+        browser.setAttribute("width", "500");
+        browser.setAttribute("height", "500");
+        panel.appendChild(browser);
+
+        let loadListener = () => {
+          panel.removeEventListener("load", loadListener);
+
+          if (this.context) {
+            this.context.unload();
+          }
+
+          this.context = new ExtensionPage(this.extension, {
+            type: "popup",
+            contentWindow: browser.contentWindow,
+            uri: Services.io.newURI(popupURL, null, null),
+            docShell: browser.docShell,
+          });
+          GlobalManager.injectInDocShell(browser.docShell, this.extension, this.context);
+          browser.setAttribute("src", popupURL);
+        };
+        panel.addEventListener("load", loadListener);
+      } else {
+        node.removeAttribute("type");
+      }
+    }
+  },
+
+  // Note: tab is allowed to be null here.
+  getIcon(tab, node) {
+    let icon = this.icon.get(tab);
+
+    let url;
+    if (typeof(icon) != "object") {
+      url = icon;
+    } else {
+      let window = node.ownerDocument.defaultView;
+      let utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                        .getInterface(Components.interfaces.nsIDOMWindowUtils);
+      let res = {value: 1}
+      utils.getResolution(res);
+
+      let size = res.value == 1 ? 19 : 38;
+      url = icon[size];
+    }
+
+    if (url) {
+      return this.extension.baseURI.resolve(url);
+    } else {
+      return "chrome://browser/content/extension.svg";
+    }
+  },
+
+  // Update the toolbar button for a given window.
+  updateWindow(window) {
+    let tab = window.gBrowser ? window.gBrowser.selectedTab : null;
+    let node = CustomizableUI.getWidget(this.id).forWindow(window).node;
+    this.updateTab(tab, node);
+  },
+
+  // Update the toolbar button when the extension changes the icon,
+  // title, badge, etc. If it only changes a parameter for a single
+  // tab, |tab| will be that tab. Otherwise it will be null.
+  updateOnChange(tab) {
+    if (tab) {
+      if (tab.selected) {
+        this.updateWindow(tab.ownerDocument.defaultView);
+      }
+    } else {
+      let e = Services.wm.getEnumerator("navigator:browser");
+      while (e.hasMoreElements()) {
+        let window = e.getNext();
+        if (window.gBrowser) {
+          this.updateWindow(window);
+        }
+      }
+    }
+  },
+
+  // tab is allowed to be null.
+  // prop should be one of "icon", "title", "badgeText", "popup", or "badgeBackgroundColor".
+  setProperty(tab, prop, value) {
+    this[prop].set(tab, value);
+    this.updateOnChange(tab);
+  },
+
+  // tab is allowed to be null.
+  // prop should be one of "title", "badgeText", "popup", or "badgeBackgroundColor".
+  getProperty(tab, prop) {
+    return this[prop].get(tab);
+  },
+
+  shutdown() {
+    CustomizableUI.destroyWidget(this.id);
+  },
+};
+
+EventEmitter.decorate(BrowserAction.prototype);
+
+extensions.on("manifest_browser_action", (type, directive, extension, manifest) => {
+  let browserAction = new BrowserAction(manifest.browser_action, extension);
+  browserAction.build();
+  browserActionMap.set(extension, browserAction);
+});
+
+extensions.on("shutdown", (type, extension) => {
+  if (browserActionMap.has(extension)) {
+    browserActionMap.get(extension).shutdown();
+    browserActionMap.delete(extension);
+  }
+});
+
+extensions.registerAPI((extension, context) => {
+  return {
+    browserAction: {
+      onClicked: new EventManager(context, "browserAction.onClicked", fire => {
+        let listener = () => {
+          let tab = TabManager.activeTab;
+          fire(TabManager.convert(extension, tab));
+        };
+        browserActionOf(extension).on("click", listener);
+        return () => {
+          browserActionOf(extension).off("click", listener);
+        };
+      }).api(),
+
+      setTitle: function(details) {
+        let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
+        browserActionOf(extension).setProperty(tab, "title", details.title);
+      },
+
+      getTitle: function(details, callback) {
+        let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
+        let title = browserActionOf(extension).getProperty(tab, "title");
+        runSafe(context, callback, title);
+      },
+
+      setIcon: function(details, callback) {
+        let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
+        if (details.imageData) {
+          // FIXME: Support the imageData attribute.
+          return;
+        }
+        browserActionOf(extension).setProperty(tab, "icon", details.path);
+      },
+
+      setBadgeText: function(details) {
+        let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
+        browserActionOf(extension).setProperty(tab, "badgeText", details.text);
+      },
+
+      getBadgeText: function(details, callback) {
+        let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
+        let text = browserActionOf(extension).getProperty(tab, "badgeText");
+        runSafe(context, callback, text);
+      },
+
+      setPopup: function(details) {
+        let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
+        browserActionOf(extension).setProperty(tab, "popup", details.popup);
+      },
+
+      getPopup: function(details, callback) {
+        let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
+        let popup = browserActionOf(extension).getProperty(tab, "popup");
+        runSafe(context, callback, popup);
+      },
+
+      setBadgeBackgroundColor: function(details) {
+        let color = details.color;
+        let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
+        browserActionOf(extension).setProperty(tab, "badgeBackgroundColor", details.color);
+      },
+
+      getBadgeBackgroundColor: function(details, callback) {
+        let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
+        let color = browserActionOf(extension).getProperty(tab, "badgeBackgroundColor");
+        runSafe(context, callback, color);
+      },
+    }
+  };
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -0,0 +1,8 @@
+extensions.registerPrivilegedAPI("contextMenus", (extension, context) => {
+  return {
+    contextMenus: {
+      create() {},
+      removeAll() {},
+    },
+  };
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-tabs.js
@@ -0,0 +1,486 @@
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabURL",
+                                  "resource:///modules/NewTabURL.jsm");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+let {
+  EventManager,
+  ignoreEvent,
+  runSafe,
+} = ExtensionUtils;
+
+// This function is pretty tightly tied to Extension.jsm.
+// Its job is to fill in the |tab| property of the sender.
+function getSender(context, target, sender)
+{
+  // The message was sent from a content script to a <browser> element.
+  // We can just get the |tab| from |target|.
+  if (target instanceof Ci.nsIDOMXULElement) {
+    // The message came from a content script.
+    let tabbrowser = target.ownerDocument.defaultView.gBrowser;
+    if (!tabbrowser) {
+      return;
+    }
+    let tab = tabbrowser.getTabForBrowser(target);
+
+    sender.tab = TabManager.convert(context.extension, tab);
+  } else {
+    // The message came from an ExtensionPage. In that case, it should
+    // include a tabId property (which is filled in by the page-open
+    // listener below).
+    if ("tabId" in sender) {
+      sender.tab = TabManager.convert(context.extension, TabManager.getTab(sender.tabId));
+      delete sender.tabId;
+    }
+  }
+}
+
+// WeakMap[ExtensionPage -> {tab, parentWindow}]
+let pageDataMap = new WeakMap();
+
+// This listener fires whenever an extension page opens in a tab
+// (either initiated by the extension or the user). Its job is to fill
+// in some tab-specific details and keep data around about the
+// ExtensionPage.
+extensions.on("page-load", (type, page, params, sender, delegate) => {
+  if (params.type == "tab") {
+    let browser = params.docShell.chromeEventHandler;
+    let parentWindow = browser.ownerDocument.defaultView;
+    let tab = parentWindow.gBrowser.getTabForBrowser(browser);
+    sender.tabId = TabManager.getId(tab);
+
+    pageDataMap.set(page, {tab, parentWindow});
+  }
+
+  delegate.getSender = getSender;
+});
+
+extensions.on("page-unload", (type, page) => {
+  pageDataMap.delete(page);
+});
+
+extensions.on("page-shutdown", (type, page) => {
+  if (pageDataMap.has(page)) {
+    let {tab, parentWindow} = pageDataMap.get(page);
+    pageDataMap.delete(page);
+
+    parentWindow.gBrowser.removeTab(tab);
+  }
+});
+
+extensions.on("fill-browser-data", (type, browser, data, result) => {
+  let tabId = TabManager.getBrowserId(browser);
+  if (tabId == -1) {
+    result.cancel = true;
+    return;
+  }
+
+  data.tabId = tabId;
+});
+
+// TODO: activeTab permission
+
+extensions.registerAPI((extension, context) => {
+  let self = {
+    tabs: {
+      onActivated: new WindowEventManager(context, "tabs.onActivated", "TabSelect", (fire, event) => {
+        let tab = event.originalTarget;
+        let tabId = TabManager.getId(tab);
+        let windowId = WindowManager.getId(tab.ownerDocument.defaultView);
+        fire({tabId, windowId});
+      }).api(),
+
+      onCreated: new EventManager(context, "tabs.onCreated", fire => {
+        let listener = event => {
+          let tab = event.originalTarget;
+          fire({tab: TabManager.convert(extension, tab)});
+        };
+
+        let windowListener = window => {
+          for (let tab of window.gBrowser.tabs) {
+            fire({tab: TabManager.convert(extension, tab)});
+          }
+        };
+
+        WindowListManager.addOpenListener(windowListener, false);
+        AllWindowEvents.addListener("TabOpen", listener);
+        return () => {
+          WindowListManager.removeOpenListener(windowListener);
+          AllWindowEvents.removeListener("TabOpen", listener);
+        };
+      }).api(),
+
+      onUpdated: new EventManager(context, "tabs.onUpdated", fire => {
+        function sanitize(extension, changeInfo) {
+          let result = {};
+          let nonempty = false;
+          for (let prop in changeInfo) {
+            if ((prop != "favIconUrl" && prop != "url") || extension.hasPermission("tabs")) {
+              nonempty = true;
+              result[prop] = changeInfo[prop];
+            }
+          }
+          return [nonempty, result];
+        }
+
+        let listener = event => {
+          let tab = event.originalTarget;
+          let window = tab.ownerDocument.defaultView;
+          let tabId = TabManager.getId(tab);
+
+          let changeInfo = {};
+          let needed = false;
+          if (event.type == "TabAttrModified") {
+            if (event.detail.changed.indexOf("image") != -1) {
+              changeInfo.favIconUrl = window.gBrowser.getIcon(tab);
+              needed = true;
+            }
+          } else if (event.type == "TabPinned") {
+            changeInfo.pinned = true;
+            needed = true;
+          } else if (event.type == "TabUnpinned") {
+            changeInfo.pinned = false;
+            needed = true;
+          }
+
+          [needed, changeInfo] = sanitize(extension, changeInfo);
+          if (needed) {
+            fire(tabId, changeInfo, TabManager.convert(extension, tab));
+          }
+        };
+        let progressListener = {
+          onStateChange(browser, webProgress, request, stateFlags, statusCode) {
+            if (!webProgress.isTopLevel) {
+              return;
+            }
+
+            let status;
+            if (stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
+              if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
+                status = "loading";
+              } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+                status = "complete";
+              }
+            } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+                       statusCode == Cr.NS_BINDING_ABORTED) {
+              status = "complete";
+            }
+
+            let gBrowser = browser.ownerDocument.defaultView.gBrowser;
+            let tab = gBrowser.getTabForBrowser(browser);
+            let tabId = TabManager.getId(tab);
+            let [needed, changeInfo] = sanitize(extension, {status});
+            fire(tabId, changeInfo, TabManager.convert(extension, tab));
+          },
+
+          onLocationChange(browser, webProgress, request, locationURI, flags) {
+            let gBrowser = browser.ownerDocument.defaultView.gBrowser;
+            let tab = gBrowser.getTabForBrowser(browser);
+            let tabId = TabManager.getId(tab);
+            let [needed, changeInfo] = sanitize(extension, {url: locationURI.spec});
+            if (needed) {
+              fire(tabId, changeInfo, TabManager.convert(extension, tab));
+            }
+          },
+        };
+
+        AllWindowEvents.addListener("progress", progressListener);
+        AllWindowEvents.addListener("TabAttrModified", listener);
+        AllWindowEvents.addListener("TabPinned", listener);
+        AllWindowEvents.addListener("TabUnpinned", listener);
+        return () => {
+          AllWindowEvents.removeListener("progress", progressListener);
+          AllWindowEvents.addListener("TabAttrModified", listener);
+          AllWindowEvents.addListener("TabPinned", listener);
+          AllWindowEvents.addListener("TabUnpinned", listener);
+        };
+      }).api(),
+
+      onReplaced: ignoreEvent(),
+
+      onRemoved: new EventManager(context, "tabs.onRemoved", fire => {
+        let tabListener = event => {
+          let tab = event.originalTarget;
+          let tabId = TabManager.getId(tab);
+          let windowId = WindowManager.getId(tab.ownerDocument.defaultView);
+          let removeInfo = {windowId, isWindowClosing: false};
+          fire(tabId, removeInfo);
+        };
+
+        let windowListener = window => {
+          for (let tab of window.gBrowser.tabs) {
+            let tabId = TabManager.getId(tab);
+            let windowId = WindowManager.getId(window);
+            let removeInfo = {windowId, isWindowClosing: true};
+            fire(tabId, removeInfo);
+          }
+        };
+
+        WindowListManager.addCloseListener(windowListener);
+        AllWindowEvents.addListener("TabClose", tabListener);
+        return () => {
+          WindowListManager.removeCloseListener(windowListener);
+          AllWindowEvents.removeListener("TabClose", tabListener);
+        };
+      }).api(),
+
+      create: function(createProperties, callback) {
+        if (!createProperties) {
+          createProperties = {};
+        }
+
+        let url = createProperties.url || NewTabURL.get();
+        url = extension.baseURI.resolve(url);
+
+        function createInWindow(window) {
+          let tab = window.gBrowser.addTab(url);
+
+          let active = true;
+          if ("active" in createProperties) {
+            active = createProperties.active;
+          } else if ("selected" in createProperties) {
+            active = createProperties.selected;
+          }
+          if (active) {
+            window.gBrowser.selectedTab = tab;
+          }
+
+          if ("index" in createProperties) {
+            window.gBrowser.moveTabTo(tab, createProperties.index);
+          }
+
+          if (createProperties.pinned) {
+            window.gBrowser.pinTab(tab);
+          }
+
+          if (callback) {
+            runSafe(context, callback, TabManager.convert(extension, tab));
+          }
+        }
+
+        let window = createProperties.windowId ?
+          WindowManager.getWindow(createProperties.windowId) :
+          WindowManager.topWindow;
+        if (!window.gBrowser) {
+          let obs = (finishedWindow, topic, data) => {
+            if (finishedWindow != window) {
+              return;
+            }
+            Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
+            createInWindow(window);
+          };
+          Services.obs.addObserver(obs, "browser-delayed-startup-finished", false);
+        } else {
+          createInWindow(window);
+        }
+      },
+
+      remove: function(tabs, callback) {
+        if (!Array.isArray(tabs)) {
+          tabs = [tabs];
+        }
+
+        for (let tabId of tabs) {
+          let tab = TabManager.getTab(tabId);
+          tab.ownerDocument.defaultView.gBrowser.removeTab(tab);
+        }
+
+        if (callback) {
+          runSafe(context, callback);
+        }
+      },
+
+      update: function(...args) {
+        let tabId, updateProperties, callback;
+        if (args.length == 1) {
+          updateProperties = args[0];
+        } else {
+          [tabId, updateProperties, callback] = args;
+        }
+
+        let tab = tabId ? TabManager.getTab(tabId) : TabManager.activeTab;
+        let tabbrowser = tab.ownerDocument.gBrowser;
+        if ("url" in updateProperties) {
+          tab.linkedBrowser.loadURI(updateProperties.url);
+        }
+        if ("active" in updateProperties) {
+          if (updateProperties.active) {
+            tabbrowser.selectedTab = tab;
+          } else {
+            // Not sure what to do here? Which tab should we select?
+          }
+        }
+        if ("pinned" in updateProperties) {
+          if (updateProperties.pinned) {
+            tabbrowser.pinTab(tab);
+          } else {
+            tabbrowser.unpinTab(tab);
+          }
+        }
+        // FIXME: highlighted/selected, openerTabId
+
+        if (callback) {
+          runSafe(context, callback, TabManager.convert(extension, tab));
+        }
+      },
+
+      reload: function(tabId, reloadProperties, callback) {
+        let tab = tabId ? TabManager.getTab(tabId) : TabManager.activeTab;
+        let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+        if (reloadProperties && reloadProperties.bypassCache) {
+          flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
+        }
+        tab.linkedBrowser.reloadWithFlags(flags);
+
+        if (callback) {
+          runSafe(context, callback);
+        }
+      },
+
+      get: function(tabId, callback) {
+        let tab = TabManager.getTab(tabId);
+        runSafe(context, callback, TabManager.convert(extension, tab));
+      },
+
+      getAllInWindow: function(...args) {
+        let window, callback;
+        if (args.length == 1) {
+          callbacks = args[0];
+        } else {
+          window = WindowManager.getWindow(args[0]);
+          callback = args[1];
+        }
+
+        if (!window) {
+          window = WindowManager.topWindow;
+        }
+
+        return self.tabs.query({windowId: WindowManager.getId(window)}, callback);
+      },
+
+      query: function(queryInfo, callback) {
+        if (!queryInfo) {
+          queryInfo = {};
+        }
+
+        function matches(window, tab) {
+          let props = ["active", "pinned", "highlighted", "status", "title", "url", "index"];
+          for (let prop of props) {
+            if (prop in queryInfo && queryInfo[prop] != tab[prop]) {
+              return false;
+            }
+          }
+
+          let lastFocused = window == WindowManager.topWindow;
+          if ("lastFocusedWindow" in queryInfo && queryInfo.lastFocusedWindow != lastFocused) {
+            return false;
+          }
+
+          let windowType = WindowManager.windowType(window);
+          if ("windowType" in queryInfo && queryInfo.windowType != windowType) {
+            return false;
+          }
+
+          if ("windowId" in queryInfo) {
+            if (queryInfo.windowId == WindowManager.WINDOW_ID_CURRENT) {
+              if (context.contentWindow != window) {
+                return false;
+              }
+            } else {
+              if (queryInfo.windowId != tab.windowId) {
+                return false;
+              }
+            }
+          }
+
+          if ("currentWindow" in queryInfo) {
+            let eq = window == context.contentWindow;
+            if (queryInfo.currentWindow != eq) {
+              return false;
+            }
+          }
+
+          return true;
+        }
+
+        let result = [];
+        let e = Services.wm.getEnumerator("navigator:browser");
+        while (e.hasMoreElements()) {
+          let window = e.getNext();
+          let tabs = TabManager.getTabs(extension, window);
+          for (let tab of tabs) {
+            if (matches(window, tab)) {
+              result.push(tab);
+            }
+          }
+        }
+        runSafe(context, callback, result);
+      },
+
+      _execute: function(tabId, details, kind, callback) {
+        let tab = tabId ? TabManager.getTab(tabId) : TabManager.activeTab;
+        let mm = tab.linkedBrowser.messageManager;
+
+        let options = {js: [], css: []};
+        if (details.code) {
+          options[kind + 'Code'] = details.code;
+        }
+        if (details.file) {
+          options[kind].push(extension.baseURI.resolve(details.file));
+        }
+        if (details.allFrames) {
+          options.all_frames = details.allFrames;
+        }
+        if (details.matchAboutBlank) {
+          options.match_about_blank = details.matchAboutBlank;
+        }
+        if (details.runAt) {
+          options.run_at = details.runAt;
+        }
+        mm.sendAsyncMessage("Extension:Execute",
+                            {extensionId: extension.id, options});
+
+        // TODO: Call the callback with the result (which is what???).
+      },
+
+      executeScript: function(...args) {
+        if (args.length == 1) {
+          self.tabs._execute(undefined, args[0], 'js', undefined);
+        } else {
+          self.tabs._execute(args[0], args[1], 'js', args[2]);
+        }
+      },
+
+      insertCss: function(tabId, details, callback) {
+        if (args.length == 1) {
+          self.tabs._execute(undefined, args[0], 'css', undefined);
+        } else {
+          self.tabs._execute(args[0], args[1], 'css', args[2]);
+        }
+      },
+
+      connect: function(tabId, connectInfo) {
+        let tab = TabManager.getTab(tabId);
+        let mm = tab.linkedBrowser.messageManager;
+
+        let name = connectInfo.name || "";
+        let recipient = {extensionId: extension.id};
+        if ("frameId" in connectInfo) {
+          recipient.frameId = connectInfo.frameId;
+        }
+        return context.messenger.connect(mm, name, recipient);
+      },
+
+      sendMessage: function(tabId, message, options, responseCallback) {
+        let tab = TabManager.getTab(tabId);
+        let mm = tab.linkedBrowser.messageManager;
+
+        let recipient = {extensionId: extension.id};
+        if (options && "frameId" in options) {
+          recipient.frameId = options.frameId;
+        }
+        return context.messenger.sendMessage(mm, message, recipient, responseCallback);
+      },
+    },
+  };
+  return self;
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-utils.js
@@ -0,0 +1,324 @@
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+let {
+  EventManager,
+} = ExtensionUtils;
+
+// This file provides some useful code for the |tabs| and |windows|
+// modules. All of the code is installed on |global|, which is a scope
+// shared among the different ext-*.js scripts.
+
+// Manages mapping between XUL tabs and extension tab IDs.
+global.TabManager = {
+  _tabs: new WeakMap(),
+  _nextId: 1,
+
+  getId(tab) {
+    if (this._tabs.has(tab)) {
+      return this._tabs.get(tab);
+    }
+    let id = this._nextId++;
+    this._tabs.set(tab, id);
+    return id;
+  },
+
+  getBrowserId(browser) {
+    let gBrowser = browser.ownerDocument.defaultView.gBrowser;
+    // Some non-browser windows have gBrowser but not
+    // getTabForBrowser!
+    if (gBrowser && gBrowser.getTabForBrowser) {
+      let tab = gBrowser.getTabForBrowser(browser);
+      if (tab) {
+        return this.getId(tab);
+      }
+    }
+    return -1;
+  },
+
+  getTab(tabId) {
+    // FIXME: Speed this up without leaking memory somehow.
+    let e = Services.wm.getEnumerator("navigator:browser");
+    while (e.hasMoreElements()) {
+      let window = e.getNext();
+      if (!window.gBrowser) {
+        continue;
+      }
+      for (let tab of window.gBrowser.tabs) {
+        if (this.getId(tab) == tabId) {
+          return tab;
+        }
+      }
+    }
+    return null;
+  },
+
+  get activeTab() {
+    let window = WindowManager.topWindow;
+    if (window && window.gBrowser) {
+      return window.gBrowser.selectedTab;
+    }
+    return null;
+  },
+
+  getStatus(tab) {
+    return tab.getAttribute("busy") == "true" ? "loading" : "complete";
+  },
+
+  convert(extension, tab) {
+    let window = tab.ownerDocument.defaultView;
+    let windowActive = window == WindowManager.topWindow;
+    let result = {
+      id: this.getId(tab),
+      index: tab._tPos,
+      windowId: WindowManager.getId(window),
+      selected: tab.selected,
+      highlighted: tab.selected,
+      active: tab.selected,
+      pinned: tab.pinned,
+      status: this.getStatus(tab),
+      incognito: PrivateBrowsingUtils.isBrowserPrivate(tab.linkedBrowser),
+      width: tab.linkedBrowser.clientWidth,
+      height: tab.linkedBrowser.clientHeight,
+    };
+
+    if (extension.hasPermission("tabs")) {
+      result.url = tab.linkedBrowser.currentURI.spec;
+      if (tab.linkedBrowser.contentTitle) {
+        result.title = tab.linkedBrowser.contentTitle;
+      }
+      let icon = window.gBrowser.getIcon(tab);
+      if (icon) {
+        result.favIconUrl = icon;
+      }
+    }
+
+    return result;
+  },
+
+  getTabs(extension, window) {
+    if (!window.gBrowser) {
+      return [];
+    }
+    return [ for (tab of window.gBrowser.tabs) this.convert(extension, tab) ];
+  },
+};
+
+// Manages mapping between XUL windows and extension window IDs.
+global.WindowManager = {
+  _windows: new WeakMap(),
+  _nextId: 0,
+
+  WINDOW_ID_NONE: -1,
+  WINDOW_ID_CURRENT: -2,
+
+  get topWindow() {
+    return Services.wm.getMostRecentWindow("navigator:browser");
+  },
+
+  windowType(window) {
+    // TODO: Make this work.
+    return "normal";
+  },
+
+  getId(window) {
+    if (this._windows.has(window)) {
+      return this._windows.get(window);
+    }
+    let id = this._nextId++;
+    this._windows.set(window, id);
+    return id;
+  },
+
+  getWindow(id) {
+    let e = Services.wm.getEnumerator("navigator:browser");
+    while (e.hasMoreElements()) {
+      let window = e.getNext();
+      if (this.getId(window) == id) {
+        return window;
+      }
+    }
+    return null;
+  },
+
+  convert(extension, window, getInfo) {
+    let result = {
+      id: this.getId(window),
+      focused: window == WindowManager.topWindow,
+      top: window.screenY,
+      left: window.screenX,
+      width: window.outerWidth,
+      height: window.outerHeight,
+      incognito: PrivateBrowsingUtils.isWindowPrivate(window),
+
+      // We fudge on these next two.
+      type: this.windowType(window),
+      state: window.fullScreen ? "fullscreen" : "normal",
+    };
+
+    if (getInfo && getInfo.populate) {
+      results.tabs = TabManager.getTabs(extension, window);
+    }
+
+    return result;
+  },
+};
+
+// Manages listeners for window opening and closing. A window is
+// considered open when the "load" event fires on it. A window is
+// closed when a "domwindowclosed" notification fires for it.
+global.WindowListManager = {
+  _openListeners: new Set(),
+  _closeListeners: new Set(),
+
+  addOpenListener(listener, fireOnExisting = true) {
+    if (this._openListeners.length == 0 && this._closeListeners.length == 0) {
+      Services.ww.registerNotification(this);
+    }
+    this._openListeners.add(listener);
+
+    let e = Services.wm.getEnumerator("navigator:browser");
+    while (e.hasMoreElements()) {
+      let window = e.getNext();
+      if (window.document.readyState != "complete") {
+        window.addEventListener("load", this);
+      } else if (fireOnExisting) {
+        listener(window);
+      }
+    }
+  },
+
+  removeOpenListener(listener) {
+    this._openListeners.delete(listener);
+    if (this._openListeners.length == 0 && this._closeListeners.length == 0) {
+      Services.ww.unregisterNotification(this);
+    }
+  },
+
+  addCloseListener(listener) {
+    if (this._openListeners.length == 0 && this._closeListeners.length == 0) {
+      Services.ww.registerNotification(this);
+    }
+    this._closeListeners.add(listener);
+  },
+
+  removeCloseListener(listener) {
+    this._closeListeners.delete(listener);
+    if (this._openListeners.length == 0 && this._closeListeners.length == 0) {
+      Services.ww.unregisterNotification(this);
+    }
+  },
+
+  handleEvent(event) {
+    let window = event.target.defaultView;
+    window.removeEventListener("load", this.loadListener);
+    if (window.document.documentElement.getAttribute("windowtype") != "navigator:browser") {
+      return;
+    }
+
+    for (let listener of this._openListeners) {
+      listener(window);
+    }
+  },
+
+  queryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
+
+  observe(window, topic, data) {
+    if (topic == "domwindowclosed") {
+      if (window.document.documentElement.getAttribute("windowtype") != "navigator:browser") {
+        return;
+      }
+
+      window.removeEventListener("load", this);
+      for (let listener of this._closeListeners) {
+        listener(window);
+      }
+    } else {
+      window.addEventListener("load", this);
+    }
+  },
+};
+
+// Provides a facility to listen for DOM events across all XUL windows.
+global.AllWindowEvents = {
+  _listeners: new Map(),
+
+  // If |type| is a normal event type, invoke |listener| each time
+  // that event fires in any open window. If |type| is "progress", add
+  // a web progress listener that covers all open windows.
+  addListener(type, listener) {
+    if (type == "domwindowopened") {
+      return WindowListManager.addOpenListener(listener);
+    } else if (type == "domwindowclosed") {
+      return WindowListManager.addCloseListener(listener);
+    }
+
+    let needOpenListener = this._listeners.size == 0;
+
+    if (!this._listeners.has(type)) {
+      this._listeners.set(type, new Set());
+    }
+    let list = this._listeners.get(type);
+    list.add(listener);
+
+    if (needOpenListener) {
+      WindowListManager.addOpenListener(this.openListener);
+    }
+  },
+
+  removeListener(type, listener) {
+    if (type == "domwindowopened") {
+      return WindowListManager.removeOpenListener(listener);
+    } else if (type == "domwindowclosed") {
+      return WindowListManager.removeCloseListener(listener);
+    }
+
+    let listeners = this._listeners.get(type);
+    listeners.delete(listener);
+    if (listeners.length == 0) {
+      this._listeners.delete(type);
+      if (this._listeners.size == 0) {
+        WindowListManager.removeOpenListener(this.openListener);
+      }
+    }
+
+    let e = Services.wm.getEnumerator("navigator:browser");
+    while (e.hasMoreElements()) {
+      let window = e.getNext();
+      if (type == "progress") {
+        window.gBrowser.removeTabsProgressListener(listener);
+      } else {
+        window.removeEventListener(type, listener);
+      }
+    }
+  },
+
+  // Runs whenever the "load" event fires for a new window.
+  openListener(window) {
+    for (let [eventType, listeners] of AllWindowEvents._listeners) {
+      for (let listener of listeners) {
+        if (eventType == "progress") {
+          window.gBrowser.addTabsProgressListener(listener);
+        } else {
+          window.addEventListener(eventType, listener);
+        }
+      }
+    }
+  },
+};
+
+// Subclass of EventManager where we just need to call
+// add/removeEventListener on each XUL window.
+global.WindowEventManager = function(context, name, event, listener)
+{
+  EventManager.call(this, context, name, fire => {
+    let listener2 = (...args) => listener(fire, ...args);
+    AllWindowEvents.addListener(event, listener2);
+    return () => {
+      AllWindowEvents.removeListener(event, listener2);
+    }
+  });
+}
+
+WindowEventManager.prototype = Object.create(EventManager.prototype);
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-windows.js
@@ -0,0 +1,151 @@
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabURL",
+                                  "resource:///modules/NewTabURL.jsm");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+let {
+  EventManager,
+  ignoreEvent,
+  runSafe,
+} = ExtensionUtils;
+
+extensions.registerAPI((extension, context) => {
+  return {
+    windows: {
+      WINDOW_ID_CURRENT: WindowManager.WINDOW_ID_CURRENT,
+      WINDOW_ID_NONE: WindowManager.WINDOW_ID_NONE,
+
+      onCreated:
+      new WindowEventManager(context, "windows.onCreated", "domwindowopened", (fire, window) => {
+        fire(WindowManager.convert(extension, window));
+      }).api(),
+
+      onRemoved:
+      new WindowEventManager(context, "windows.onRemoved", "domwindowclosed", (fire, window) => {
+        fire(WindowManager.getId(window));
+      }).api(),
+
+      onFocusChanged: new EventManager(context, "windows.onFocusChanged", fire => {
+        // FIXME: This will send multiple messages for a single focus change.
+        let listener = event => {
+          let window = WindowManager.topWindow;
+          let windowId = window ? WindowManager.getId(window) : WindowManager.WINDOW_ID_NONE;
+          fire(windowId);
+        };
+        AllWindowEvents.addListener("focus", listener);
+        AllWindowEvents.addListener("blur", listener);
+        return () => {
+          AllWindowEvents.removeListener("focus", listener);
+          AllWindowEvents.removeListener("blur", listener);
+        };
+      }).api(),
+
+      get: function(windowId, getInfo, callback) {
+        let window = WindowManager.getWindow(windowId);
+        runSafe(context, callback, WindowManager.convert(extension, window, getInfo));
+      },
+
+      getCurrent: function(getInfo, callback) {
+        let window = context.contentWindow;
+        runSafe(context, callback, WindowManager.convert(extension, window, getInfo));
+      },
+
+      getLastFocused: function(...args) {
+        let getInfo, callback;
+        if (args.length == 1) {
+          callback = args[0];
+        } else {
+          [getInfo, callback] = args;
+        }
+        let window = WindowManager.topWindow;
+        runSafe(context, callback, WindowManager.convert(extension, window, getInfo));
+      },
+
+      getAll: function(getAll, callback) {
+        let e = Services.wm.getEnumerator("navigator:browser");
+        let windows = [];
+        while (e.hasMoreElements()) {
+          let window = e.getNext();
+          windows.push(WindowManager.convert(extension, window, getInfo));
+        }
+        runSafe(context, callback, windows);
+      },
+
+      create: function(createData, callback) {
+        function mkstr(s) {
+          let result = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+          result.data = s;
+          return result;
+        }
+
+        let args = Cc["@mozilla.org/supports-array;1"].createInstance(Ci.nsISupportsArray);
+        if ("url" in createData) {
+          if (Array.isArray(createData.url)) {
+            let array = Cc["@mozilla.org/supports-array;1"].createInstance(Ci.nsISupportsArray);
+            for (let url of createData.url) {
+              array.AppendElement(mkstr(url));
+            }
+            args.AppendElement(array);
+          } else {
+            args.AppendElement(mkstr(createData.url));
+          }
+        } else {
+          args.AppendElement(mkstr(NewTabURL.get()));
+        }
+
+        let extraFeatures = "";
+        if ("incognito" in createData) {
+          if (createData.incognito) {
+            extraFeatures += ",private";
+          } else {
+            extraFeatures += ",non-private";
+          }
+        }
+
+        let window = Services.ww.openWindow(null, "chrome://browser/content/browser.xul", "_blank",
+                                            "chrome,dialog=no,all" + extraFeatures, args);
+
+        if ("left" in createData || "top" in createData) {
+          let left = "left" in createData ? createData.left : window.screenX;
+          let top = "top" in createData ? createData.top : window.screenY;
+          window.moveTo(left, top);
+        }
+        if ("width" in createData || "height" in createData) {
+          let width = "width" in createData ? createData.width : window.outerWidth;
+          let height = "height" in createData ? createData.height : window.outerHeight;
+          window.resizeTo(width, height);
+        }
+
+        // TODO: focused, type, state
+
+        window.addEventListener("load", function listener() {
+          window.removeEventListener("load", listener);
+          if (callback) {
+            runSafe(context, callback, WindowManager.convert(extension, window));
+          }
+        });
+      },
+
+      update: function(windowId, updateInfo, callback) {
+        let window = WindowManager.getWindow(windowId);
+        if (updateInfo.focused) {
+          Services.focus.activeWindow = window;
+        }
+        // TODO: All the other properties...
+        runSafe(context, callback, WindowManager.convert(extension, window));
+      },
+
+      remove: function(windowId, callback) {
+        let window = WindowManager.getWindow(windowId);
+        window.close();
+
+        let listener = () => {
+          AllWindowEvents.removeListener("domwindowclosed", listener);
+          if (callback) {
+            runSafe(context, callback);
+          }
+        };
+        AllWindowEvents.addListener("domwindowclosed", listener);
+      },
+    },
+  };
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/extension.svg
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+     width="64" height="64" viewBox="0 0 64 64">
+  <defs>
+    <style>
+      .style-puzzle-piece {
+        fill: url('#gradient-linear-puzzle-piece');
+      }
+    </style>
+    <linearGradient id="gradient-linear-puzzle-piece" x1="0%" y1="0%" x2="0%" y2="100%">
+      <stop offset="0%" stop-color="#66cc52" stop-opacity="1"/>
+      <stop offset="100%" stop-color="#60bf4c" stop-opacity="1"/>
+    </linearGradient>
+  </defs>
+  <path class="style-puzzle-piece" d="M42,62c2.2,0,4-1.8,4-4l0-14.2c0,0,0.4-3.7,2.8-3.7c2.4,0,2.2,3.9,6.7,3.9c2.3,0,6.2-1.2,6.2-8.2 c0-7-3.9-7.9-6.2-7.9c-4.5,0-4.3,3.7-6.7,3.7c-2.4,0-2.8-3.8-2.8-3.8V22c0-2.2-1.8-4-4-4H31.5c0,0-3.4-0.6-3.4-3 c0-2.4,3.8-2.6,3.8-7.1c0-2.3-1.3-5.9-8.3-5.9s-8,3.6-8,5.9c0,4.5,3.4,4.7,3.4,7.1c0,2.4-3.4,3-3.4,3H6c-2.2,0-4,1.8-4,4l0,7.8 c0,0-0.4,6,4.4,6c3.1,0,3.2-4.1,7.3-4.1c2,0,4,1.9,4,6c0,4.2-2,6.3-4,6.3c-4,0-4.2-4.1-7.3-4.1c-4.8,0-4.4,5.8-4.4,5.8L2,58 c0,2.2,1.8,4,4,4H19c0,0,6.3,0.4,6.3-4.4c0-3.1-4-3.6-4-7.7c0-2,2.2-4.5,6.4-4.5c4.2,0,6.6,2.5,6.6,4.5c0,4-3.9,4.6-3.9,7.7 c0,4.9,6.3,4.4,6.3,4.4H42z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/jar.mn
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+browser.jar:
+    content/browser/extension.svg (extension.svg)
+    content/browser/ext-utils.js (ext-utils.js)
+    content/browser/ext-contextMenus.js (ext-contextMenus.js)
+    content/browser/ext-browserAction.js (ext-browserAction.js)
+    content/browser/ext-tabs.js (ext-tabs.js)
+    content/browser/ext-windows.js (ext-windows.js)
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ['jar.mn']
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/prepare.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import argparse
+import json
+import uuid
+import sys
+import os.path
+
+parser = argparse.ArgumentParser(description='Create install.rdf from manifest.json')
+parser.add_argument('--locale')
+parser.add_argument('--profile')
+parser.add_argument('--uuid')
+parser.add_argument('dir')
+args = parser.parse_args()
+
+manifestFile = os.path.join(args.dir, 'manifest.json')
+manifest = json.load(open(manifestFile))
+
+locale = args.locale
+if not locale:
+    locale = manifest.get('default_locale', 'en-US')
+
+def process_locale(s):
+    if s.startswith('__MSG_') and s.endswith('__'):
+        tag = s[6:-2]
+        path = os.path.join(args.dir, '_locales', locale, 'messages.json')
+        data = json.load(open(path))
+        return data[tag]['message']
+    else:
+        return s
+
+id = args.uuid
+if not id:
+    id = '{' + str(uuid.uuid4()) + '}'
+
+name = process_locale(manifest['name'])
+desc = process_locale(manifest['description'])
+version = manifest['version']
+
+installFile = open(os.path.join(args.dir, 'install.rdf'), 'w')
+print >>installFile, '<?xml version="1.0"?>'
+print >>installFile, '<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"'
+print >>installFile, '     xmlns:em="http://www.mozilla.org/2004/em-rdf#">'
+print >>installFile
+print >>installFile, '  <Description about="urn:mozilla:install-manifest">'
+print >>installFile, '    <em:id>{}</em:id>'.format(id)
+print >>installFile, '    <em:type>2</em:type>'
+print >>installFile, '    <em:name>{}</em:name>'.format(name)
+print >>installFile, '    <em:description>{}</em:description>'.format(desc)
+print >>installFile, '    <em:version>{}</em:version>'.format(version)
+print >>installFile, '    <em:bootstrap>true</em:bootstrap>'
+
+print >>installFile, '    <em:targetApplication>'
+print >>installFile, '      <Description>'
+print >>installFile, '        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>'
+print >>installFile, '        <em:minVersion>4.0</em:minVersion>'
+print >>installFile, '        <em:maxVersion>50.0</em:maxVersion>'
+print >>installFile, '      </Description>'
+print >>installFile, '    </em:targetApplication>'
+
+print >>installFile, '  </Description>'
+print >>installFile, '</RDF>'
+installFile.close()
+
+bootstrapPath = os.path.join(os.path.dirname(sys.argv[0]), 'bootstrap.js')
+data = open(bootstrapPath).read()
+boot = open(os.path.join(args.dir, 'bootstrap.js'), 'w')
+boot.write(data)
+boot.close()
+
+if args.profile:
+    os.system('mkdir -p {}/extensions'.format(args.profile))
+    output = open(args.profile + '/extensions/' + id, 'w')
+    print >>output, os.path.realpath(args.dir)
+    output.close()
+else:
+    dir = os.path.realpath(args.dir)
+    if dir[-1] == os.sep:
+        dir = dir[:-1]
+    os.system('cd "{}"; zip ../"{}".xpi -r *'.format(args.dir, os.path.basename(dir)))
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -4,16 +4,17 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DIRS += [
     'about',
     'customizableui',
     'dirprovider',
     'downloads',
+    'extensions',
     'feeds',
     'loop',
     'migration',
     'places',
     'pocket',
     'preferences',
     'privatebrowsing',
     'readinglist',
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -164,16 +164,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource:///modules/ReaderParent.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonWatcher",
                                   "resource://gre/modules/AddonWatcher.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
                                   "resource://gre/modules/LightweightThemeManager.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
+                                  "resource://gre/modules/ExtensionManagement.jsm");
+
 const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser";
 const PREF_PLUGINS_UPDATEURL  = "plugins.update.url";
 
 // Seconds of idle before trying to create a bookmarks backup.
 const BOOKMARKS_BACKUP_IDLE_TIME_SEC = 8 * 60;
 // Minimum interval between backups.  We try to not create more than one backup
 // per interval.
 const BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS = 1;
@@ -596,16 +599,22 @@ BrowserGlue.prototype = {
 #endif
     os.addObserver(this, "browser-search-engine-modified", false);
     os.addObserver(this, "browser-search-service", false);
     os.addObserver(this, "restart-in-safe-mode", false);
     os.addObserver(this, "flash-plugin-hang", false);
     os.addObserver(this, "xpi-signature-changed", false);
     os.addObserver(this, "autocomplete-did-enter-text", false);
 
+    ExtensionManagement.registerScript("chrome://browser/content/ext-utils.js");
+    ExtensionManagement.registerScript("chrome://browser/content/ext-browserAction.js");
+    ExtensionManagement.registerScript("chrome://browser/content/ext-contextMenus.js");
+    ExtensionManagement.registerScript("chrome://browser/content/ext-tabs.js");
+    ExtensionManagement.registerScript("chrome://browser/content/ext-windows.js");
+
     this._flashHangCount = 0;
   },
 
   // cleanup (called on application shutdown)
   _dispose: function BG__dispose() {
     let os = Services.obs;
     os.removeObserver(this, "prefservice:after-app-defaults");
     os.removeObserver(this, "final-ui-startup");
--- a/browser/modules/E10SUtils.jsm
+++ b/browser/modules/E10SUtils.jsm
@@ -54,16 +54,25 @@ this.E10SUtils = {
     if (aURL.startsWith("chrome:")) {
       let url = Services.io.newURI(aURL, null, null);
       let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
                       getService(Ci.nsIXULChromeRegistry);
       canLoadRemote = chromeReg.canLoadURLRemotely(url);
       mustLoadRemote = chromeReg.mustLoadURLRemotely(url);
     }
 
+    if (aURL.startsWith("moz-extension:")) {
+      canLoadRemote = false;
+      mustLoadRemote = false;
+    }
+
+    if (aURL.startsWith("view-source:")) {
+      return this.canLoadURIInProcess(aURL.substr("view-source:".length), aProcess);
+    }
+
     if (mustLoadRemote)
       return processIsRemote;
 
     if (!canLoadRemote && processIsRemote)
       return false;
 
     return true;
   },
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1946,8 +1946,12 @@ chatbox {
 %include ../shared/contextmenu.inc.css
 
 #context-navigation > .menuitem-iconic > .menu-iconic-left {
   visibility: visible;
   /* override toolkit/themes/linux/global/menu.css */
   -moz-padding-end: 0 !important;
   -moz-margin-end: 0 !important;
 }
+
+.browser-action-panel > .panel-arrowcontainer > .panel-arrowcontent {
+  padding: 0;
+}
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -3687,8 +3687,12 @@ window > chatbox {
 }
 
 %include ../shared/contextmenu.inc.css
 
 #context-navigation > .menuitem-iconic {
   padding-left: 0;
   padding-right: 0;
 }
+
+.browser-action-panel > .panel-arrowcontainer > .panel-arrowcontent {
+  padding: 0;
+}
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -2907,8 +2907,12 @@ chatbox {
   -moz-margin-start: -28px;
   margin-top: -4px;
 }
 
 
 @media not all and (-moz-os-version: windows-xp) {
 %include browser-aero.css
 }
+
+.browser-action-panel > .panel-arrowcontainer > .panel-arrowcontent {
+  padding: 0;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/Extension.jsm
@@ -0,0 +1,578 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["Extension"];
+
+/*
+ * This file is the main entry point for extensions. When an extension
+ * loads, its bootstrap.js file creates a Extension instance
+ * and calls .startup() on it. It calls .shutdown() when the extension
+ * unloads. Extension manages any extension-specific state in
+ * the chrome process.
+ */
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/devtools/event-emitter.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Locale",
+                                  "resource://gre/modules/Locale.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
+                                  "resource://gre/modules/MatchPattern.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+                                  "resource://gre/modules/NetUtil.jsm");
+
+Cu.import("resource://gre/modules/ExtensionManagement.jsm");
+
+// Register built-in parts of the API. Other parts may be registered
+// in browser/, mobile/, or b2g/.
+ExtensionManagement.registerScript("chrome://extensions/content/ext-alarms.js");
+ExtensionManagement.registerScript("chrome://extensions/content/ext-backgroundPage.js");
+ExtensionManagement.registerScript("chrome://extensions/content/ext-notifications.js");
+ExtensionManagement.registerScript("chrome://extensions/content/ext-i18n.js");
+ExtensionManagement.registerScript("chrome://extensions/content/ext-idle.js");
+ExtensionManagement.registerScript("chrome://extensions/content/ext-runtime.js");
+ExtensionManagement.registerScript("chrome://extensions/content/ext-extension.js");
+ExtensionManagement.registerScript("chrome://extensions/content/ext-webNavigation.js");
+ExtensionManagement.registerScript("chrome://extensions/content/ext-webRequest.js");
+ExtensionManagement.registerScript("chrome://extensions/content/ext-storage.js");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+let {
+  MessageBroker,
+  Messenger,
+  injectAPI,
+} = ExtensionUtils;
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+let scriptScope = this;
+
+// This object loads the ext-*.js scripts that define the extension API.
+let Management = {
+  initialized: false,
+  scopes: [],
+  apis: [],
+  emitter: new EventEmitter(),
+
+  // Loads all the ext-*.js scripts currently registered.
+  lazyInit() {
+    if (this.initialized) {
+      return;
+    }
+    this.initialized = true;
+
+    for (let script of ExtensionManagement.getScripts()) {
+      let scope = {extensions: this, global: scriptScope};
+      Services.scriptloader.loadSubScript(script, scope, "UTF-8");
+
+      // Save the scope to avoid it being garbage collected.
+      this.scopes.push(scope);
+    }
+  },
+
+  // Called by an ext-*.js script to register an API. The |api|
+  // parameter should be an object of the form:
+  // {
+  //   tabs: {
+  //     create: ...,
+  //     onCreated: ...
+  //   }
+  // }
+  // This registers tabs.create and tabs.onCreated as part of the API.
+  registerAPI(api) {
+    this.apis.push({api});
+  },
+
+  // Same as above, but only register the API is the add-on has the
+  // given permission.
+  registerPrivilegedAPI(permission, api) {
+    this.apis.push({api, permission});
+  },
+
+  // Mash together into a single object all the APIs registered by the
+  // functions above. Return the merged object.
+  generateAPIs(extension, context) {
+    let obj = {};
+
+    // Recursively copy properties from source to dest.
+    function copy(dest, source) {
+      for (let prop in source) {
+        if (typeof(source[prop]) == "object") {
+          if (!(prop in dest)) {
+            dest[prop] = {};
+          }
+          copy(dest[prop], source[prop]);
+        } else {
+          dest[prop] = source[prop];
+        }
+      }
+    }
+
+    for (let api of this.apis) {
+      if (api.permission) {
+        if (!extension.hasPermission(api.permission)) {
+          continue;
+        }
+      }
+
+      api = api.api(extension, context);
+      copy(obj, api);
+    }
+
+    return obj;
+  },
+
+  // The ext-*.js scripts can ask to be notified for certain hooks.
+  on(hook, callback) {
+    this.emitter.on(hook, callback);
+  },
+
+  // Ask to run all the callbacks that are registered for a given hook.
+  emit(hook, ...args) {
+    this.lazyInit();
+    this.emitter.emit(hook, ...args);
+  },
+};
+
+// A MessageBroker that's used to send and receive messages for
+// extension pages (which run in the chrome process).
+let globalBroker = new MessageBroker([Services.mm, Services.ppmm]);
+
+// An extension page is an execution context for any extension content
+// that runs in the chrome process. It's used for background pages
+// (type="background"), popups (type="popup"), and any extension
+// content loaded into browser tabs (type="tab").
+//
+// |params| is an object with the following properties:
+// |type| is one of "background", "popup", or "tab".
+// |contentWindow| is the DOM window the content runs in.
+// |uri| is the URI of the content (optional).
+// |docShell| is the docshell the content runs in (optional).
+function ExtensionPage(extension, params)
+{
+  let {type, contentWindow, uri, docShell} = params;
+  this.extension = extension;
+  this.type = type;
+  this.contentWindow = contentWindow || null;
+  this.uri = uri || extension.baseURI;
+  this.onClose = new Set();
+
+  // This is the sender property passed to the Messenger for this
+  // page. It can be augmented by the "page-open" hook.
+  let sender = {id: extension.id};
+  if (uri) {
+    sender.url = uri.spec;
+  }
+  let delegate = {};
+  Management.emit("page-load", this, params, sender, delegate);
+
+  let filter = {id: extension.id};
+  this.messenger = new Messenger(this, globalBroker, sender, filter, delegate);
+
+  this.extension.views.add(this);
+}
+
+ExtensionPage.prototype = {
+  get cloneScope() {
+    return this.contentWindow;
+  },
+
+  callOnClose(obj) {
+    this.onClose.add(obj);
+  },
+
+  forgetOnClose(obj) {
+    this.onClose.delete(obj);
+  },
+
+  // Called when the extension shuts down.
+  shutdown() {
+    Management.emit("page-shutdown", this);
+    this.unload();
+  },
+
+  // This method is called when an extension page navigates away or
+  // its tab is closed.
+  unload() {
+    Management.emit("page-unload", this);
+
+    this.extension.views.delete(this);
+
+    for (let obj of this.onClose) {
+      obj.close();
+    }
+  },
+};
+
+// Responsible for loading extension APIs into the right globals.
+let GlobalManager = {
+  // Number of extensions currently enabled.
+  count: 0,
+
+  // Map[docShell -> {extension, context}] where context is an ExtensionPage.
+  docShells: new Map(),
+
+  // Map[extension ID -> Extension]. Determines which extension is
+  // responsible for content under a particular extension ID.
+  extensionMap: new Map(),
+
+  init(extension) {
+    if (this.count == 0) {
+      Services.obs.addObserver(this, "content-document-global-created", false);
+    }
+    this.count++;
+
+    this.extensionMap.set(extension.id, extension);
+  },
+
+  uninit(extension) {
+    this.count--;
+    if (this.count == 0) {
+      Services.obs.removeObserver(this, "content-document-global-created");
+    }
+
+    for (let [docShell, data] of this.docShells) {
+      if (extension == data.extension) {
+        this.docShells.delete(docShell);
+      }
+    }
+
+    this.extensionMap.delete(extension.id);
+  },
+
+  injectInDocShell(docShell, extension, context) {
+    this.docShells.set(docShell, {extension, context});
+  },
+
+  observe(contentWindow, topic, data) {
+    function inject(extension, context) {
+      let chromeObj = Cu.createObjectIn(contentWindow, {defineAs: "chrome"});
+      let api = Management.generateAPIs(extension, context);
+      injectAPI(api, chromeObj);
+    }
+
+    let docShell = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                                .getInterface(Ci.nsIWebNavigation)
+                                .QueryInterface(Ci.nsIDocShellTreeItem)
+                                .sameTypeRootTreeItem
+                                .QueryInterface(Ci.nsIDocShell);
+
+    if (this.docShells.has(docShell)) {
+      let {extension, context} = this.docShells.get(docShell);
+      inject(extension, context);
+      return;
+    }
+
+    // We don't inject into sub-frames of a UI page.
+    if (contentWindow != contentWindow.top) {
+      return;
+    }
+
+    // Find the add-on associated with this document via the
+    // principal's originAttributes. This value is computed by
+    // extensionURIToAddonID, which ensures that we don't inject our
+    // API into webAccessibleResources.
+    let principal = contentWindow.document.nodePrincipal;
+    let id = principal.originAttributes.addonId;
+    if (!this.extensionMap.has(id)) {
+      return;
+    }
+    let extension = this.extensionMap.get(id);
+    let uri = contentWindow.document.documentURIObject;
+    let context = new ExtensionPage(extension, {type: "tab", contentWindow, uri, docShell});
+    inject(extension, context);
+
+    let eventHandler = docShell.chromeEventHandler;
+    let listener = event => {
+      eventHandler.removeEventListener("unload", listener);
+      context.unload();
+    };
+    eventHandler.addEventListener("unload", listener, true);
+  },
+};
+
+// We create one instance of this class per extension. |addonData|
+// comes directly from bootstrap.js when initializing.
+function Extension(addonData)
+{
+  let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+  let uuid = uuidGenerator.generateUUID().number;
+  uuid = uuid.substring(1, uuid.length - 1); // Strip of { and } off the UUID.
+  this.uuid = uuid;
+
+  this.addonData = addonData;
+  this.id = addonData.id;
+  this.baseURI = Services.io.newURI("moz-extension://" + uuid, null, null);
+  this.manifest = null;
+  this.localeMessages = null;
+
+  this.views = new Set();
+
+  this.onStartup = null;
+
+  this.hasShutdown = false;
+  this.onShutdown = new Set();
+
+  this.permissions = new Set();
+  this.whiteListedHosts = null;
+  this.webAccessibleResources = new Set();
+
+  ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this);
+}
+
+Extension.prototype = {
+  // Representation of the extension to send to content
+  // processes. This should include anything the content process might
+  // need.
+  serialize() {
+    return {
+      id: this.id,
+      uuid: this.uuid,
+      manifest: this.manifest,
+      resourceURL: this.addonData.resourceURI.spec,
+      baseURL: this.baseURI.spec,
+      content_scripts: this.manifest.content_scripts || [],
+      webAccessibleResources: this.webAccessibleResources,
+      whiteListedHosts: this.whiteListedHosts.serialize(),
+    };
+  },
+
+  // https://developer.chrome.com/extensions/i18n
+  localizeMessage(message, substitutions) {
+    if (message in this.localeMessages) {
+      let str = this.localeMessages[message].message;
+
+      if (!substitutions) {
+        substitutions = [];
+      }
+      if (!Array.isArray(substitutions)) {
+        substitutions = [substitutions];
+      }
+
+      // https://developer.chrome.com/extensions/i18n-messages
+      // |str| may contain substrings of the form $1 or $PLACEHOLDER$.
+      // In the former case, we replace $n with substitutions[n - 1].
+      // In the latter case, we consult the placeholders array.
+      // The placeholder may itself use $n to refer to substitutions.
+      let replacer = (matched, name) => {
+        if (name.length == 1 && name[0] >= '1' && name[0] <= '9') {
+          return substitutions[parseInt(name) - 1];
+        } else {
+          let content = this.localeMessages[message].placeholders[name].content;
+          if (content[0] == '$') {
+            return replacer(matched, content[1]);
+          } else {
+            return content;
+          }
+        }
+      };
+      return str.replace(/\$([A-Za-z_@]+)\$/, replacer)
+                .replace(/\$([0-9]+)/, replacer)
+                .replace(/\$\$/, "$");
+    }
+
+    // Check for certain pre-defined messages.
+    if (message == "@@extension_id") {
+      return this.id;
+    } else if (message == "@@ui_locale") {
+      return Locale.getLocale();
+    } else if (message == "@@bidi_dir") {
+      return "ltr"; // FIXME
+    }
+
+    Cu.reportError(`Unknown localization message ${message}`);
+    return "??";
+  },
+
+  localize(str) {
+    if (!str) {
+      return str;
+    }
+
+    if (str.startsWith("__MSG_") && str.endsWith("__")) {
+      let message = str.substring("__MSG_".length, str.length - "__".length);
+      return this.localizeMessage(message);
+    }
+
+    return str;
+  },
+
+  readJSON(uri) {
+    return new Promise((resolve, reject) => {
+      NetUtil.asyncFetch({uri, loadUsingSystemPrincipal: true}, (inputStream, status) => {
+        if (!Components.isSuccessCode(status)) {
+          reject(status);
+          return;
+        }
+        let text = NetUtil.readInputStreamToString(inputStream, inputStream.available());
+        try {
+          resolve(JSON.parse(text));
+        } catch (e) {
+          reject(e);
+        }
+      });
+    });
+  },
+
+  readManifest() {
+    let manifestURI = Services.io.newURI("manifest.json", null, this.baseURI);
+    return this.readJSON(manifestURI);
+  },
+
+  readLocaleFile(locale) {
+    let dir = locale.replace("-", "_");
+    let url = `_locales/${dir}/messages.json`;
+    let uri = Services.io.newURI(url, null, this.baseURI);
+    return this.readJSON(uri);
+  },
+
+  readLocaleMessages() {
+    let locales = [];
+
+    // We need to base this off of this.addonData.resourceURI rather
+    // than baseURI since baseURI is a moz-extension URI, which always
+    // QIs to nsIFileURL.
+    let uri = Services.io.newURI("_locales", null, this.addonData.resourceURI);
+    if (uri instanceof Ci.nsIFileURL) {
+      let file = uri.file;
+      let enumerator;
+      try {
+        enumerator = file.directoryEntries;
+      } catch (e) {
+        return {};
+      }
+      while (enumerator.hasMoreElements()) {
+        let file = enumerator.getNext().QueryInterface(Ci.nsIFile);
+        locales.push({
+          name: file.leafName,
+          locales: [file.leafName.replace("_", "-")]
+        });
+      }
+    }
+
+    if (uri instanceof Ci.nsIJARURI && uri.JARFile instanceof Ci.nsIFileURL) {
+      let file = uri.JARFile.file;
+      let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader);
+      try {
+        zipReader.open(file);
+        let enumerator = zipReader.findEntries("_locales/*");
+        while (enumerator.hasMore()) {
+          let name = enumerator.getNext();
+          let match = name.match(new RegExp("_locales\/([^/]*)"));
+          if (match && match[1]) {
+            locales.push({
+              name: match[1],
+              locales: [match[1].replace("_", "-")]
+            });
+          }
+        }
+      } finally {
+        zipReader.close();
+      }
+    }
+
+    let locale = Locale.findClosestLocale(locales);
+    if (locale) {
+      return this.readLocaleFile(locale.name).catch(() => {});
+    }
+    return {};
+  },
+
+  runManifest(manifest) {
+    let permissions = manifest.permissions || [];
+    let webAccessibleResources = manifest.web_accessible_resources || [];
+
+    let whitelist = [];
+    for (let perm of permissions) {
+      if (perm.match(/:\/\//)) {
+        whitelist.push(perm);
+      } else {
+        this.permissions.add(perm);
+      }
+    }
+    this.whiteListedHosts = new MatchPattern(whitelist);
+
+    let resources = new Set();
+    for (let url of webAccessibleResources) {
+      resources.add(url);
+    }
+    this.webAccessibleResources = resources;
+
+    for (let directive in manifest) {
+      Management.emit("manifest_" + directive, directive, this, manifest);
+    }
+
+    let data = Services.ppmm.initialProcessData;
+    if (!data["Extension:Extensions"]) {
+      data["Extension:Extensions"] = [];
+    }
+    let serial = this.serialize();
+    data["Extension:Extensions"].push(serial);
+    Services.ppmm.broadcastAsyncMessage("Extension:Startup", serial);
+  },
+
+  callOnClose(obj) {
+    this.onShutdown.add(obj);
+  },
+
+  forgetOnClose(obj) {
+    this.onShutdown.delete(obj);
+  },
+
+  startup() {
+    GlobalManager.init(this);
+
+    return Promise.all([this.readManifest(), this.readLocaleMessages()]).then(([manifest, messages]) => {
+      if (this.hasShutdown) {
+        return;
+      }
+
+      this.manifest = manifest;
+      this.localeMessages = messages;
+
+      Management.emit("startup", this);
+
+      this.runManifest(manifest);
+    }).catch(e => {
+      dump(`Extension error: ${e} ${e.fileName}:${e.lineNumber}\n`);
+      Cu.reportError(e);
+    });
+  },
+
+  shutdown() {
+    this.hasShutdown = true;
+    if (!this.manifest) {
+      return;
+    }
+
+    GlobalManager.uninit(this);
+
+    for (let view of this.views) {
+      view.shutdown();
+    }
+
+    for (let obj of this.onShutdown) {
+      obj.close();
+    }
+
+    Management.emit("shutdown", this);
+
+    Services.ppmm.broadcastAsyncMessage("Extension:Shutdown", {id: this.id});
+
+    ExtensionManagement.shutdownExtension(this.uuid);
+  },
+
+  hasPermission(perm) {
+    return this.permissions.has(perm);
+  },
+};
+
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -0,0 +1,521 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["ExtensionContent"];
+
+/*
+ * This file handles the content process side of extensions. It mainly
+ * takes care of content script injection, content script APIs, and
+ * messaging.
+ */
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
+                                  "resource://gre/modules/ExtensionManagement.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
+                                  "resource://gre/modules/MatchPattern.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+let {
+  runSafeWithoutClone,
+  MessageBroker,
+  Messenger,
+  ignoreEvent,
+  injectAPI,
+} = ExtensionUtils;
+
+function isWhenBeforeOrSame(when1, when2)
+{
+  let table = {"document_start": 0,
+               "document_end": 1,
+               "document_idle": 2};
+  return table[when1] <= table[when2];
+}
+
+// This is the fairly simple API that we inject into content
+// scripts.
+let api = context => { return {
+  runtime: {
+    connect: function(extensionId, connectInfo) {
+      let name = connectInfo && connectInfo.name || "";
+      let recipient = extensionId ? {extensionId} : {extensionId: context.extensionId};
+      return context.messenger.connect(context.messageManager, name, recipient);
+    },
+
+    getManifest: function(context) {
+      return context.extension.getManifest();
+    },
+
+    getURL: function(path) {
+      return context.extension.baseURI.resolve(url);
+    },
+
+    onConnect: context.messenger.onConnect("runtime.onConnect"),
+
+    onMessage: context.messenger.onMessage("runtime.onMessage"),
+
+    sendMessage: function(...args) {
+      let extensionId, message, options, responseCallback;
+      if (args.length == 1) {
+        message = args[0];
+      } else if (args.length == 2) {
+        [message, responseCallback] = args;
+      } else {
+        [extensionId, message, options, responseCallback] = args;
+      }
+
+      let recipient = extensionId ? {extensionId} : {extensionId: context.extensionId};
+      context.messenger.sendMessage(context.messageManager, message, recipient, responseCallback);
+    },
+  },
+
+  extension: {
+    getURL: function(path) {
+      return context.extension.baseURI.resolve(url);
+    },
+
+    inIncognitoContext: PrivateBrowsingUtils.isContentWindowPrivate(context.contentWindow),
+  },
+}};
+
+// Represents a content script.
+function Script(options)
+{
+  this.options = options;
+  this.run_at = this.options.run_at;
+  this.js = this.options.js || [];
+  this.css = this.options.css || [];
+
+  this.matches_ = new MatchPattern(this.options.matches);
+  this.exclude_matches_ = new MatchPattern(this.options.exclude_matches || null);
+
+  // TODO: Support glob patterns.
+}
+
+Script.prototype = {
+  matches(window) {
+    let uri = window.document.documentURIObject;
+    if (!this.matches_.matches(uri)) {
+      return false;
+    }
+
+    if (this.exclude_matches_.matches(uri)) {
+      return false;
+    }
+
+    if (!this.options.all_frames && window.top != window) {
+      return false;
+    }
+
+    // TODO: match_about_blank.
+
+    return true;
+  },
+
+  tryInject(extension, window, sandbox, shouldRun) {
+    if (!this.matches(window)) {
+      return;
+    }
+
+    if (shouldRun("document_start")) {
+      let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor).
+        getInterface(Ci.nsIDOMWindowUtils);
+
+      for (let url of this.css) {
+        url = extension.baseURI.resolve(url);
+        runSafeWithoutClone(winUtils.loadSheetUsingURIString, url, winUtils.AUTHOR_SHEET);
+      }
+
+      if (this.options.cssCode) {
+        let url = "data:text/css;charset=utf-8," + encodeURIComponent(this.options.cssCode);
+        runSafeWithoutClone(winUtils.loadSheetUsingURIString, url, winUtils.AUTHOR_SHEET);
+      }
+    }
+
+    let scheduled = this.run_at || "document_idle";
+    if (shouldRun(scheduled)) {
+      for (let url of this.js) {
+        url = extension.baseURI.resolve(url);
+        Services.scriptloader.loadSubScript(url, sandbox);
+      }
+
+      if (this.options.jsCode) {
+        Cu.evalInSandbox(this.options.jsCode, sandbox, "latest");
+      }
+    }
+  },
+};
+
+function getWindowMessageManager(contentWindow)
+{
+  let ir = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                        .getInterface(Ci.nsIDocShell)
+                        .QueryInterface(Ci.nsIInterfaceRequestor);
+  try {
+    return ir.getInterface(Ci.nsIContentFrameMessageManager);
+  } catch (e) {
+    // Some windows don't support this interface (hidden window).
+    return null;
+  }
+}
+
+// Scope in which extension content script code can run. It uses
+// Cu.Sandbox to run the code. There is a separate scope for each
+// frame.
+function ExtensionContext(extensionId, contentWindow)
+{
+  this.extension = ExtensionManager.get(extensionId);
+  this.extensionId = extensionId;
+  this.contentWindow = contentWindow;
+
+  let utils = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                           .getInterface(Ci.nsIDOMWindowUtils);
+  let outerWindowId = utils.outerWindowID;
+  let frameId = contentWindow == contentWindow.top ? 0 : outerWindowId;
+  this.frameId = frameId;
+
+  let mm = getWindowMessageManager(contentWindow);
+  this.messageManager = mm;
+
+  let prin = [contentWindow];
+  if (Services.scriptSecurityManager.isSystemPrincipal(contentWindow.document.nodePrincipal)) {
+    // Make sure we don't hand out the system principal by accident.
+    prin = Cc["@mozilla.org/nullprincipal;1"].createInstance(Ci.nsIPrincipal);
+  }
+
+  this.sandbox = Cu.Sandbox(prin, {sandboxPrototype: contentWindow, wantXrays: true});
+
+  let delegate = {
+    getSender(context, target, sender) {
+      // Nothing to do here.
+    }
+  };
+
+  let url = contentWindow.location.href;
+  let broker = ExtensionContent.getBroker(mm);
+  this.messenger = new Messenger(this, broker, {id: extensionId, frameId, url},
+                                 {id: extensionId, frameId}, delegate);
+
+  let chromeObj = Cu.createObjectIn(this.sandbox, {defineAs: "chrome"});
+  injectAPI(api(this), chromeObj);
+
+  this.onClose = new Set();
+}
+
+ExtensionContext.prototype = {
+  get cloneScope() {
+    return this.sandbox;
+  },
+
+  execute(script, shouldRun) {
+    script.tryInject(this.extension, this.contentWindow, this.sandbox, shouldRun);
+  },
+
+  callOnClose(obj) {
+    this.onClose.add(obj);
+  },
+
+  forgetOnClose(obj) {
+    this.onClose.delete(obj);
+  },
+
+  close() {
+    for (let obj of this.onClose) {
+      obj.close();
+    }
+  },
+};
+
+// Responsible for creating ExtensionContexts and injecting content
+// scripts into them when new documents are created.
+let DocumentManager = {
+  extensionCount: 0,
+
+  // WeakMap[window -> Map[extensionId -> ExtensionContext]]
+  windows: new WeakMap(),
+
+  init() {
+    Services.obs.addObserver(this, "document-element-inserted", false);
+    Services.obs.addObserver(this, "dom-window-destroyed", false);
+  },
+
+  uninit() {
+    Services.obs.removeObserver(this, "document-element-inserted");
+    Services.obs.removeObserver(this, "dom-window-destroyed");
+  },
+
+  getWindowState(contentWindow) {
+    let readyState = contentWindow.document.readyState;
+    if (readyState == "loading") {
+      return "document_start";
+    } else if (readyState == "interactive") {
+      return "document_end";
+    } else {
+      return "document_idle";
+    }
+  },
+
+  observe: function(subject, topic, data) {
+    if (topic == "document-element-inserted") {
+      let document = subject;
+      let window = document && document.defaultView;
+      if (!document || !document.location || !window) {
+        return;
+      }
+
+      // Make sure we only load into frames that ExtensionContent.init
+      // was called on (i.e., not frames for social or sidebars).
+      let mm = getWindowMessageManager(window);
+      if (!mm || !ExtensionContent.globals.has(mm)) {
+        return;
+      }
+
+      this.windows.delete(window);
+
+      this.trigger("document_start", window);
+      window.addEventListener("DOMContentLoaded", this, true);
+      window.addEventListener("load", this, true);
+    } else if (topic == "dom-window-destroyed") {
+      let window = subject;
+      if (!this.windows.has(window)) {
+        return;
+      }
+
+      let extensions = this.windows.get(window);
+      for (let [extensionId, context] of extensions) {
+        context.close();
+      }
+
+      this.windows.delete(window);
+    }
+  },
+
+  handleEvent: function(event) {
+    let window = event.target.defaultView;
+    window.removeEventListener(event.type, this, true);
+
+    // Need to check if we're still on the right page? Greasemonkey does this.
+
+    if (event.type == "DOMContentLoaded") {
+      this.trigger("document_end", window);
+    } else if (event.type == "load") {
+      this.trigger("document_idle", window);
+    }
+  },
+
+  executeScript(global, extensionId, script) {
+    let window = global.content;
+    let extensions = this.windows.get(window);
+    if (!extensions) {
+      return;
+    }
+    let context = extensions.get(extensionId);
+    if (!context) {
+      return;
+    }
+
+    // TODO: Somehow make sure we have the right permissions for this origin!
+    // FIXME: Need to keep this around so that I will execute it later if we're not in the right state.
+    context.execute(script, scheduled => scheduled == state);
+  },
+
+  enumerateWindows: function*(docShell) {
+    let window = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIDOMWindow)
+    yield [window, this.getWindowState(window)];
+
+    for (let i = 0; i < docShell.childCount; i++) {
+      let child = docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell);
+      yield* this.enumerateWindows(child);
+    }
+  },
+
+  getContext(extensionId, window) {
+    if (!this.windows.has(window)) {
+      this.windows.set(window, new Map());
+    }
+    let extensions = this.windows.get(window);
+    if (!extensions.has(extensionId)) {
+      let context = new ExtensionContext(extensionId, window);
+      extensions.set(extensionId, context);
+    }
+    return extensions.get(extensionId);
+  },
+
+  startupExtension(extensionId) {
+    if (this.extensionCount == 0) {
+      this.init();
+    }
+    this.extensionCount++;
+
+    let extension = ExtensionManager.get(extensionId);
+    for (let global of ExtensionContent.globals.keys()) {
+      for (let [window, state] of this.enumerateWindows(global.docShell)) {
+        for (let script of extension.scripts) {
+          if (script.matches(window)) {
+            let context = this.getContext(extensionId, window);
+            context.execute(script, scheduled => isWhenBeforeOrSame(scheduled, state));
+          }
+        }
+      }
+    }
+  },
+
+  shutdownExtension(extensionId) {
+    for (let global of ExtensionContent.globals.keys()) {
+      for (let [window, state] of this.enumerateWindows(global.docShell)) {
+        let extensions = this.windows.get(window);
+        if (!extensions) {
+          continue;
+        }
+        let context = extensions.get(extensionId);
+        if (context) {
+          context.close();
+          extensions.delete(extensionId);
+        }
+      }
+    }
+
+    this.extensionCount--;
+    if (this.extensionCount == 0) {
+      this.uninit();
+    }
+  },
+
+  trigger(when, window) {
+    let state = this.getWindowState(window);
+    for (let [extensionId, extension] of ExtensionManager.extensions) {
+      for (let script of extension.scripts) {
+        if (script.matches(window)) {
+          let context = this.getContext(extensionId, window);
+          context.execute(script, scheduled => isWhenBeforeOrSame(scheduled, state));
+        }
+      }
+    }
+  },
+};
+
+// Represents a browser extension in the content process.
+function BrowserExtensionContent(data)
+{
+  this.id = data.id;
+  this.uuid = data.uuid;
+  this.data = data;
+  this.scripts = [ for (scriptData of data.content_scripts) new Script(scriptData) ];
+  this.webAccessibleResources = data.webAccessibleResources;
+  this.whiteListedHosts = data.whiteListedHosts;
+
+  this.manifest = data.manifest;
+  this.baseURI = Services.io.newURI(data.baseURL, null, null);
+
+  let uri = Services.io.newURI(data.resourceURL, null, null);
+
+  if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+    // Extension.jsm takes care of this in the parent.
+    ExtensionManagement.startupExtension(this.uuid, uri, this);
+  }
+};
+
+BrowserExtensionContent.prototype = {
+  shutdown() {
+    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+      ExtensionManagement.shutdownExtension(this.uuid);
+    }
+  },
+};
+
+let ExtensionManager = {
+  // Map[extensionId, BrowserExtensionContent]
+  extensions: new Map(),
+
+  init() {
+    Services.cpmm.addMessageListener("Extension:Startup", this);
+    Services.cpmm.addMessageListener("Extension:Shutdown", this);
+
+    if (Services.cpmm.initialProcessData && "Extension:Extensions" in Services.cpmm.initialProcessData) {
+      let extensions = Services.cpmm.initialProcessData["Extension:Extensions"];
+      for (let data of extensions) {
+        this.extensions.set(data.id, new BrowserExtensionContent(data));
+        DocumentManager.startupExtension(data.id);
+      }
+    }
+  },
+
+  get(extensionId) {
+    return this.extensions.get(extensionId);
+  },
+
+  receiveMessage({name, data}) {
+    let extension;
+    switch (name) {
+    case "Extension:Startup":
+      extension = new BrowserExtensionContent(data);
+      this.extensions.set(data.id, extension);
+      DocumentManager.startupExtension(data.id);
+      break;
+
+    case "Extension:Shutdown":
+      extension = this.extensions.get(data.id);
+      extension.shutdown();
+      DocumentManager.shutdownExtension(data.id);
+      this.extensions.delete(data.id);
+      break;
+    }
+  }
+};
+
+let ExtensionContent = {
+  globals: new Map(),
+
+  init(global) {
+    let broker = new MessageBroker([global]);
+    this.globals.set(global, broker);
+
+    global.addMessageListener("Extension:Execute", this);
+
+    let windowId = global.content
+                         .QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIDOMWindowUtils)
+                         .outerWindowID;
+    global.sendAsyncMessage("Extension:TopWindowID", {windowId});
+  },
+
+  uninit(global) {
+    this.globals.delete(global);
+
+    let windowId = global.content
+                         .QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIDOMWindowUtils)
+                         .outerWindowID;
+    global.sendAsyncMessage("Extension:RemoveTopWindowID", {windowId});
+  },
+
+  getBroker(messageManager) {
+    return this.globals.get(messageManager);
+  },
+
+  receiveMessage({target, name, data}) {
+    switch (name) {
+    case "Extension:Execute":
+      data.options.matches = "<all_urls>";
+      let script = new Script(data.options);
+      let {extensionId} = data;
+      DocumentManager.executeScript(target, extensionId, script);
+      break;
+    }
+  },
+};
+
+ExtensionManager.init();
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionManagement.jsm
@@ -0,0 +1,201 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["ExtensionManagement"];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+/*
+ * This file should be kept short and simple since it's loaded even
+ * when no extensions are running.
+ */
+
+// Keep track of frame IDs for content windows. Mostly we can just use
+// the outer window ID as the frame ID. However, the API specifies
+// that top-level windows have a frame ID of 0. So we need to keep
+// track of which windows are top-level. This code listens to messages
+// from ExtensionContent to do that.
+let Frames = {
+  // Window IDs of top-level content windows.
+  topWindowIds: new Set(),
+
+  init() {
+    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+      return;
+    }
+
+    Services.mm.addMessageListener("Extension:TopWindowID", this);
+    Services.mm.addMessageListener("Extension:RemoveTopWindowID", this);
+  },
+
+  isTopWindowId(windowId) {
+    return this.topWindowIds.has(windowId);
+  },
+
+  // Convert an outer window ID to a frame ID. An outer window ID of 0
+  // is invalid.
+  getId(windowId) {
+    if (this.isTopWindowId(windowId)) {
+      return 0;
+    } else if (windowId == 0) {
+      return -1;
+    } else {
+      return windowId;
+    }
+  },
+
+  // Convert an outer window ID for a parent window to a frame
+  // ID. Outer window IDs follow the same convention that
+  // |window.top.parent === window.top|. The API works differently,
+  // giving a frame ID of -1 for the the parent of a top-level
+  // window. This function handles the conversion.
+  getParentId(parentWindowId, windowId) {
+    if (parentWindowId == windowId) {
+      // We have a top-level window.
+      return -1;
+    }
+
+    // Not a top-level window. Just return the ID as normal.
+    return this.getId(parentWindowId);
+  },
+
+  receiveMessage({name, data}) {
+    switch (name) {
+    case "Extension:TopWindowID":
+      // FIXME: Need to handle the case where the content process
+      // crashes. Right now we leak its top window IDs.
+      this.topWindowIds.add(data.windowId);
+      break;
+
+    case "Extension:RemoveTopWindowID":
+      this.topWindowIds.delete(data.windowId);
+      break;
+    }
+  },
+};
+Frames.init();
+
+// Manage the collection of ext-*.js scripts that define the extension API.
+let Scripts = {
+  scripts: new Set(),
+
+  register(script) {
+    this.scripts.add(script);
+  },
+
+  getScripts() {
+    return this.scripts;
+  },
+};
+
+// This object manages various platform-level issues related to
+// moz-extension:// URIs. It lives here so that it can be used in both
+// the parent and child processes.
+//
+// moz-extension URIs have the form moz-extension://uuid/path. Each
+// extension has its own UUID, unique to the machine it's installed
+// on. This is easier and more secure than using the extension ID,
+// since it makes it slightly harder to fingerprint for extensions if
+// each user uses different URIs for the extension.
+let Service = {
+  initialized: false,
+
+  // Map[uuid -> extension].
+  // extension can be an Extension (parent process) or BrowserExtensionContent (child process).
+  uuidMap: new Map(),
+
+  init() {
+    let aps = Cc["@mozilla.org/addons/policy-service;1"].getService(Ci.nsIAddonPolicyService);
+    aps = aps.wrappedJSObject;
+    this.aps = aps;
+    aps.setExtensionURILoadCallback(this.extensionURILoadableByAnyone.bind(this));
+    aps.setExtensionURIToAddonIdCallback(this.extensionURIToAddonID.bind(this));
+  },
+
+  // Called when a new extension is loaded.
+  startupExtension(uuid, uri, extension) {
+    if (!this.initialized) {
+      this.initialized = true;
+      this.init();
+    }
+
+    // Create the moz-extension://uuid mapping.
+    let handler = Services.io.getProtocolHandler("moz-extension");
+    handler.QueryInterface(Ci.nsISubstitutingProtocolHandler);
+    handler.setSubstitution(uuid, uri);
+
+    this.uuidMap.set(uuid, extension);
+    this.aps.setAddonLoadURICallback(extension.id, this.checkAddonMayLoad.bind(this, extension));
+  },
+
+  // Called when an extension is unloaded.
+  shutdownExtension(uuid) {
+    let extension = this.uuidMap.get(uuid);
+    this.uuidMap.delete(uuid);
+    this.aps.setAddonLoadURICallback(extension.id, null);
+
+    let handler = Services.io.getProtocolHandler("moz-extension");
+    handler.QueryInterface(Ci.nsISubstitutingProtocolHandler);
+    handler.setSubstitution(uuid, null);
+  },
+
+  // Return true if the given URI can be loaded from arbitrary web
+  // content. The manifest.json |web_accessible_resources| directive
+  // determines this.
+  extensionURILoadableByAnyone(uri) {
+    let uuid = uri.host;
+    let extension = this.uuidMap.get(uuid);
+    if (!extension) {
+      return false;
+    }
+
+    let path = uri.path;
+    if (path.length > 0 && path[0] == '/') {
+      path = path.substr(1);
+    }
+    return extension.webAccessibleResources.has(path);
+  },
+
+  // Checks whether a given extension can load this URI (typically via
+  // an XML HTTP request). The manifest.json |permissions| directive
+  // determines this.
+  checkAddonMayLoad(extension, uri) {
+    return extension.whiteListedHosts.matchesIgnoringPath(uri);
+  },
+
+  // Finds the add-on ID associated with a given moz-extension:// URI.
+  // This is used to set the addonId on the originAttributes for the
+  // nsIPrincipal attached to the URI.
+  extensionURIToAddonID(uri) {
+    if (this.extensionURILoadableByAnyone(uri)) {
+      // We don't want webAccessibleResources to be associated with
+      // the add-on. That way they don't get any special privileges.
+      return null;
+    }
+
+    let uuid = uri.host;
+    let extension = this.uuidMap.get(uuid);
+    return extension ? extension.id : undefined;
+  },
+};
+
+let ExtensionManagement = {
+  startupExtension: Service.startupExtension.bind(Service),
+  shutdownExtension: Service.shutdownExtension.bind(Service),
+
+  registerScript: Scripts.register.bind(Scripts),
+  getScripts: Scripts.getScripts.bind(Scripts),
+
+  getFrameId: Frames.getId.bind(Frames),
+  getParentFrameId: Frames.getParentId.bind(Frames),
+};
+
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionStorage.jsm
@@ -0,0 +1,149 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["ExtensionStorage"];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/osfile.jsm")
+Cu.import("resource://gre/modules/AsyncShutdown.jsm");
+
+let Path = OS.Path;
+let profileDir = OS.Constants.Path.profileDir;
+
+let ExtensionStorage = {
+  cache: new Map(),
+  listeners: new Map(),
+
+  extensionDir: Path.join(profileDir, "browser-extension-data"),
+
+  getExtensionDir(extensionId) {
+    return Path.join(this.extensionDir, extensionId);
+  },
+
+  getStorageFile(extensionId) {
+    return Path.join(this.extensionDir, extensionId, "storage.js");
+  },
+
+  read(extensionId) {
+    if (this.cache.has(extensionId)) {
+      return this.cache.get(extensionId);
+    }
+
+    let path = this.getStorageFile(extensionId);
+    let decoder = new TextDecoder();
+    let promise = OS.File.read(path);
+    promise = promise.then(array => {
+      return JSON.parse(decoder.decode(array));
+    }).catch(() => {
+      Cu.reportError("Unable to parse JSON data for extension storage.");
+      return {};
+    });
+    this.cache.set(extensionId, promise);
+    return promise;
+  },
+
+  write(extensionId) {
+    let promise = this.read(extensionId).then(extData => {
+      let encoder = new TextEncoder();
+      let array = encoder.encode(JSON.stringify(extData));
+      let path = this.getStorageFile(extensionId);
+      OS.File.makeDir(this.getExtensionDir(extensionId), {ignoreExisting: true, from: profileDir});
+      let promise = OS.File.writeAtomic(path, array);
+      return promise;
+    }).catch(() => {
+      // Make sure this promise is never rejected.
+      Cu.reportError("Unable to write JSON data for extension storage.");
+    });
+
+    AsyncShutdown.profileBeforeChange.addBlocker(
+      "ExtensionStorage: Finish writing extension data",
+      promise);
+
+    return promise.then(() => {
+      AsyncShutdown.profileBeforeChange.removeBlocker(promise);
+    });
+  },
+
+  set(extensionId, items) {
+    return this.read(extensionId).then(extData => {
+      let changes = {};
+      for (let prop in items) {
+        changes[prop] = {oldValue: extData[prop], newValue: items[prop]};
+        extData[prop] = items[prop];
+      }
+
+      let listeners = this.listeners.get(extensionId);
+      if (listeners) {
+        for (let listener of listeners) {
+          listener(changes);
+        }
+      }
+
+      return this.write(extensionId);
+    });
+  },
+
+  remove(extensionId, items) {
+    return this.read(extensionId).then(extData => {
+      let changes = {};
+      for (let prop in items) {
+        changes[prop] = {oldValue: extData[prop]};
+        delete extData[prop];
+      }
+
+      let listeners = this.listeners.get(extensionId);
+      if (listeners) {
+        for (let listener of listeners) {
+          listener(changes);
+        }
+      }
+
+      return this.write(extensionId);
+    });
+  },
+
+  get(extensionId, keys) {
+    return this.read(extensionId).then(extData => {
+      let result = {};
+      if (keys === null) {
+        Object.assign(result, extData);
+      } else if (typeof(keys) == "object") {
+        for (let prop in keys) {
+          if (prop in extData) {
+            result[prop] = extData[prop];
+          } else {
+            result[prop] = keys[prop];
+          }
+        }
+      } else if (typeof(keys) == "string") {
+        result[prop] = extData[prop] || undefined;
+      } else {
+        for (let prop of keys) {
+          result[prop] = extData[prop] || undefined;
+        }
+      }
+
+      return result;
+    });
+  },
+
+  addOnChangedListener(extensionId, listener) {
+    let listeners = this.listeners.get(extensionId) || new Set();
+    listeners.add(listener);
+    this.listeners.set(extensionId, listeners);
+  },
+
+  removeOnChangedListener(extensionId, listener) {
+    let listeners = this.listeners.get(extensionId);
+    listeners.delete(listener);
+  },
+};
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -0,0 +1,540 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["ExtensionUtils"];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// Run a function and report exceptions.
+function runSafeWithoutClone(f, ...args)
+{
+  try {
+    return f(...args);
+  } catch (e) {
+    dump(`Extension error: ${e} ${e.fileName} ${e.lineNumber}\n${e.stack}\n${Error().stack}`);
+    Cu.reportError(e);
+  }
+}
+
+// Run a function, cloning arguments into context.cloneScope, and
+// report exceptions. |f| is expected to be in context.cloneScope.
+function runSafe(context, f, ...args)
+{
+  try {
+    args = Cu.cloneInto(args, context.cloneScope);
+  } catch (e) {
+    dump(`runSafe failure\n${context.cloneScope}\n${Error().stack}`);
+  }
+  return runSafeWithoutClone(f, ...args);
+}
+
+// Similar to a WeakMap, but returns a particular default value for
+// |get| if a key is not present.
+function DefaultWeakMap(defaultValue)
+{
+  this.defaultValue = defaultValue;
+  this.weakmap = new WeakMap();
+}
+
+DefaultWeakMap.prototype = {
+  get(key) {
+    if (this.weakmap.has(key)) {
+      return this.weakmap.get(key);
+    }
+    return this.defaultValue;
+  },
+
+  set(key, value) {
+    if (key) {
+      this.weakmap.set(key, value);
+    } else {
+      this.defaultValue = value;
+    }
+  },
+};
+
+// This is a generic class for managing event listeners. Example usage:
+//
+// new EventManager(context, "api.subAPI", fire => {
+//   let listener = (...) => {
+//     // Fire any listeners registered with addListener.
+//     fire(arg1, arg2);
+//   };
+//   // Register the listener.
+//   SomehowRegisterListener(listener);
+//   return () => {
+//     // Return a way to unregister the listener.
+//     SomehowUnregisterListener(listener);
+//   };
+// }).api()
+//
+// The result is an object with addListener, removeListener, and
+// hasListener methods. |context| is an add-on scope (either an
+// ExtensionPage in the chrome process or ExtensionContext in a
+// content process). |name| is for debugging. |register| is a function
+// to register the listener. |register| is only called once, event if
+// multiple listeners are registered. |register| should return an
+// unregister function that will unregister the listener.
+function EventManager(context, name, register)
+{
+  this.context = context;
+  this.name = name;
+  this.register = register;
+  this.unregister = null;
+  this.callbacks = new Set();
+  this.registered = false;
+}
+
+EventManager.prototype = {
+  addListener(callback) {
+    if (!this.registered) {
+      this.context.callOnClose(this);
+
+      let fireFunc = this.fire.bind(this);
+      let fireWithoutClone = this.fireWithoutClone.bind(this);
+      fireFunc.withoutClone = fireWithoutClone;
+      this.unregister = this.register(fireFunc);
+    }
+    this.callbacks.add(callback);
+  },
+
+  removeListener(callback) {
+    if (!this.registered) {
+      return;
+    }
+
+    this.callbacks.delete(callback);
+    if (this.callbacks.length == 0) {
+      this.unregister();
+
+      this.context.forgetOnClose(this);
+    }
+  },
+
+  hasListener(callback) {
+    return this.callbacks.has(callback);
+  },
+
+  fire(...args) {
+    for (let callback of this.callbacks) {
+      runSafe(this.context, callback, ...args);
+    }
+  },
+
+  fireWithoutClone(...args) {
+    for (let callback of this.callbacks) {
+      runSafeWithoutClone(callback, ...args);
+    }
+  },
+
+  close() {
+    this.unregister();
+  },
+
+  api() {
+    return {
+      addListener: callback => this.addListener(callback),
+      removeListener: callback => this.removeListener(callback),
+      hasListener: callback => this.hasListener(callback),
+    };
+  },
+};
+
+// Similar to EventManager, but it doesn't try to consolidate event
+// notifications. Each addListener call causes us to register once. It
+// allows extra arguments to be passed to addListener.
+function SingletonEventManager(context, name, register)
+{
+  this.context = context;
+  this.name = name;
+  this.register = register;
+  this.unregister = new Map();
+  context.callOnClose(this);
+}
+
+SingletonEventManager.prototype = {
+  addListener(callback, ...args) {
+    let unregister = this.register(callback, ...args);
+    this.unregister.set(callback, unregister);
+  },
+
+  removeListener(callback) {
+    if (!this.unregister.has(callback)) {
+      return;
+    }
+
+    let unregister = this.unregister.get(callback);
+    this.unregister.delete(callback);
+    this.unregister();
+  },
+
+  hasListener(callback) {
+    return this.unregister.has(callback);
+  },
+
+  close() {
+    for (let unregister of this.unregister.values()) {
+      unregister();
+    }
+  },
+
+  api() {
+    return {
+      addListener: (...args) => this.addListener(...args),
+      removeListener: (...args) => this.removeListener(...args),
+      hasListener: (...args) => this.hasListener(...args),
+    };
+  },
+};
+
+// Simple API for event listeners where events never fire.
+function ignoreEvent()
+{
+  return {
+    addListener: function(context, callback) {},
+    removeListener: function(context, callback) {},
+    hasListener: function(context, callback) {},
+  };
+}
+
+// Copy an API object from |source| into the scope |dest|.
+function injectAPI(source, dest)
+{
+  for (let prop in source) {
+    // Skip names prefixed with '_'.
+    if (prop[0] == '_') {
+      continue;
+    }
+
+    let value = source[prop];
+    if (typeof(value) == "function") {
+      Cu.exportFunction(value, dest, {defineAs: prop});
+    } else if (typeof(value) == "object") {
+      let obj = Cu.createObjectIn(dest, {defineAs: prop});
+      injectAPI(value, obj);
+    } else {
+      dest[prop] = value;
+    }
+  }
+}
+
+/*
+ * Messaging primitives.
+ */
+
+let nextBrokerId = 1;
+
+let MESSAGES = [
+  "Extension:Message",
+  "Extension:Connect",
+];
+
+// Receives messages from multiple message managers and directs them
+// to a set of listeners. On the child side: one broker per frame
+// script.  On the parent side: one broker total, covering both the
+// global MM and the ppmm. Message must be tagged with a recipient,
+// which is an object with properties. Listeners can filter for
+// messages that have a certain value for a particular property in the
+// recipient. (If a message doesn't specify the given property, it's
+// considered a match.)
+function MessageBroker(messageManagers)
+{
+  this.messageManagers = messageManagers;
+  for (let mm of this.messageManagers) {
+    for (let message of MESSAGES) {
+      mm.addMessageListener(message, this);
+    }
+  }
+
+  this.listeners = {message: [], connect: []};
+}
+
+MessageBroker.prototype = {
+  uninit() {
+    for (let mm of this.messageManagers) {
+      for (let message of MESSAGES) {
+        mm.removeMessageListener(message, this);
+      }
+    }
+
+    this.listeners = null;
+  },
+
+  makeId() {
+    return nextBrokerId++;
+  },
+
+  addListener(type, listener, filter) {
+    this.listeners[type].push({filter, listener});
+  },
+
+  removeListener(type, listener) {
+    let index = -1;
+    for (let i = 0; i < this.listeners[type].length; i++) {
+      if (this.listeners[type][i].listener == listener) {
+        this.listeners[type].splice(i, 1);
+        return;
+      }
+    }
+  },
+
+  runListeners(type, target, data) {
+    let listeners = [];
+    for (let {listener, filter} of this.listeners[type]) {
+      let pass = true;
+      for (let prop in filter) {
+        if (prop in data.recipient && filter[prop] != data.recipient[prop]) {
+          pass = false;
+          break;
+        }
+      }
+
+      // Save up the list of listeners to call in case they modify the
+      // set of listeners.
+      if (pass) {
+        listeners.push(listener);
+      }
+    }
+
+    for (let listener of listeners) {
+      listener(type, target, data.message, data.sender, data.recipient);
+    }
+  },
+
+  receiveMessage({name, data, target}) {
+    switch (name) {
+    case "Extension:Message":
+      this.runListeners("message", target, data);
+      break;
+
+    case "Extension:Connect":
+      this.runListeners("connect", target, data);
+      break;
+    }
+  },
+
+  sendMessage(messageManager, type, message, sender, recipient) {
+    let data = {message, sender, recipient};
+    let names = {message: "Extension:Message", connect: "Extension:Connect"};
+    messageManager.sendAsyncMessage(names[type], data);
+  },
+};
+
+// Abstraction for a Port object in the extension API. Each port has a unique ID.
+function Port(context, messageManager, name, id, sender)
+{
+  this.context = context;
+  this.messageManager = messageManager;
+  this.name = name;
+  this.id = id;
+  this.listenerName = `Extension:Port-${this.id}`;
+  this.disconnectName = `Extension:Disconnect-${this.id}`;
+  this.sender = sender;
+  this.disconnected = false;
+}
+
+Port.prototype = {
+  api() {
+    let portObj = Cu.createObjectIn(this.context.cloneScope);
+
+    // We want a close() notification when the window is destroyed.
+    this.context.callOnClose(this);
+
+    let publicAPI = {
+      name: this.name,
+      disconnect: () => {
+        this.disconnect();
+      },
+      postMessage: json => {
+        if (this.disconnected) {
+          throw "Attempt to postMessage on disconnected port";
+        }
+        this.messageManager.sendAsyncMessage(this.listenerName, json);
+      },
+      onDisconnect: new EventManager(this.context, "Port.onDisconnect", fire => {
+        let listener = () => {
+          if (!this.disconnected) {
+            fire();
+          }
+        };
+
+        this.messageManager.addMessageListener(this.disconnectName, listener, true);
+        return () => {
+          this.messageManager.removeMessageListener(this.disconnectName, listener);
+        };
+      }).api(),
+      onMessage: new EventManager(this.context, "Port.onMessage", fire => {
+        let listener = ({data}) => {
+          if (!this.disconnected) {
+            fire(data);
+          }
+        };
+
+        this.messageManager.addMessageListener(this.listenerName, listener);
+        return () => {
+          this.messageManager.removeMessageListener(this.listenerName, listener);
+        };
+      }).api(),
+    };
+
+    if (this.sender) {
+      publicAPI.sender = this.sender;
+    }
+
+    injectAPI(publicAPI, portObj);
+    return portObj;
+  },
+
+  disconnect() {
+    this.context.forgetOnClose(this);
+    this.disconnect = true;
+    this.messageManager.sendAsyncMessage(this.disconnectName);
+  },
+
+  close() {
+    this.disconnect();
+  },
+};
+
+function getMessageManager(target)
+{
+  if (target instanceof Ci.nsIDOMXULElement) {
+    return target.messageManager;
+  } else {
+    return target;
+  }
+}
+
+// Each extension scope gets its own Messenger object. It handles the
+// basics of sendMessage, onMessage, connect, and onConnect.
+//
+// |context| is the extension scope.
+// |broker| is a MessageBroker used to receive and send messages.
+// |sender| is an object describing the sender (usually giving its extensionId, tabId, etc.)
+// |filter| is a recipient filter to apply to incoming messages from the broker.
+// |delegate| is an object that must implement a few methods:
+//    getSender(context, messageManagerTarget, sender): returns a MessageSender
+//      See https://developer.chrome.com/extensions/runtime#type-MessageSender.
+function Messenger(context, broker, sender, filter, delegate)
+{
+  this.context = context;
+  this.broker = broker;
+  this.sender = sender;
+  this.filter = filter;
+  this.delegate = delegate;
+}
+
+Messenger.prototype = {
+  sendMessage(messageManager, msg, recipient, responseCallback) {
+    let id = this.broker.makeId();
+    let replyName = `Extension:Reply-${id}`;
+    recipient.messageId = id;
+    this.broker.sendMessage(messageManager, "message", msg, this.sender, recipient);
+
+    let onClose;
+    let listener = ({data: response}) => {
+      messageManager.removeMessageListener(replyName, listener);
+      this.context.forgetOnClose(onClose);
+
+      if (response.gotData) {
+        // TODO: Handle failure to connect to the extension?
+        runSafe(this.context, responseCallback, response.data);
+      }
+    };
+    onClose = {
+      close() {
+        messageManager.removeMessageListener(replyName, listener);
+      }
+    };
+    if (responseCallback) {
+      messageManager.addMessageListener(replyName, listener);
+      this.context.callOnClose(onClose);
+    }
+  },
+
+  onMessage(name) {
+    return new EventManager(this.context, name, fire => {
+      let listener = (type, target, message, sender, recipient) => {
+        message = Cu.cloneInto(message, this.context.cloneScope);
+        if (this.delegate) {
+          this.delegate.getSender(this.context, target, sender);
+        }
+        sender = Cu.cloneInto(sender, this.context.cloneScope);
+
+        let mm = getMessageManager(target);
+        let replyName = `Extension:Reply-${recipient.messageId}`;
+
+        let valid = true, sent = false;
+        let sendResponse = data => {
+          if (!valid) {
+            return;
+          }
+          sent = true;
+          mm.sendAsyncMessage(replyName, {data, gotData: true});
+        };
+        sendResponse = Cu.exportFunction(sendResponse, this.context.cloneScope);
+
+        let result = fire.withoutClone(message, sender, sendResponse);
+        if (result !== true) {
+          valid = false;
+        }
+        if (!sent) {
+          mm.sendAsyncMessage(replyName, {gotData: false});
+        }
+      };
+
+      this.broker.addListener("message", listener, this.filter);
+      return () => {
+        this.broker.removeListener("message", listener);
+      };
+    }).api();
+  },
+
+  connect(messageManager, name, recipient) {
+    let portId = this.broker.makeId();
+    let port = new Port(this.context, messageManager, name, portId, null);
+    let msg = {name, portId};
+    this.broker.sendMessage(messageManager, "connect", msg, this.sender, recipient);
+    return port.api();
+  },
+
+  onConnect(name) {
+    return new EventManager(this.context, name, fire => {
+      let listener = (type, target, message, sender, recipient) => {
+        let {name, portId} = message;
+        let mm = getMessageManager(target);
+        if (this.delegate) {
+          this.delegate.getSender(this.context, target, sender);
+        }
+        let port = new Port(this.context, mm, name, portId, sender);
+        fire.withoutClone(port.api());
+      };
+
+      this.broker.addListener("connect", listener, this.filter);
+      return () => {
+        this.broker.removeListener("connect", listener);
+      };
+    }).api();
+  },
+};
+
+let ExtensionUtils = {
+  runSafe,
+  DefaultWeakMap,
+  EventManager,
+  SingletonEventManager,
+  ignoreEvent,
+  injectAPI,
+  MessageBroker,
+  Messenger,
+};
+
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-alarms.js
@@ -0,0 +1,168 @@
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+let {
+  EventManager,
+  ignoreEvent,
+} = ExtensionUtils;
+
+// WeakMap[Extension -> Set[Alarm]]
+let alarmsMap = new WeakMap();
+
+// WeakMap[Extension -> callback]
+let alarmCallbacksMap = new WeakMap();
+
+// Manages an alarm created by the extension (alarms API).
+function Alarm(extension, name, alarmInfo)
+{
+  this.extension = extension;
+  this.name = name;
+  this.when = alarmInfo.when;
+  this.delayInMinutes = alarmInfo.delayInMinutes;
+  this.periodInMinutes = alarmInfo.periodInMinutes;
+  this.canceled = false;
+
+  let delay, scheduledTime;
+  if (this.when) {
+    scheduledTime = this.when;
+    delay = this.when - Date.now();
+  } else {
+    delay = this.delayInMinutes * 60 * 1000;
+    scheduledTime = Date.now() + delay;
+  }
+
+  this.scheduledTime = scheduledTime;
+
+  let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+  timer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT);
+  this.timer = timer;
+}
+
+Alarm.prototype = {
+  clear() {
+    this.timer.cancel();
+    alarmsMap.get(this.extension).delete(this);
+    this.canceled = true;
+  },
+
+  observe(subject, topic, data) {
+    if (alarmCallbacksMap.has(this.extension)) {
+      alarmCallbacksMap.get(this.extension)(this);
+    }
+    if (this.canceled) {
+      return;
+    }
+
+    if (!this.periodInMinutes) {
+      this.clear();
+      return;
+    }
+
+    let delay = this.periodInMinutes * 60 * 1000;
+    this.scheduledTime = Date.now() + delay;
+    this.timer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT);
+  },
+
+  get data() {
+    return {
+      name: this.name,
+      scheduledTime: this.scheduledTime,
+      periodInMinutes: this.periodInMinutes,
+    };
+  },
+};
+
+extensions.on("startup", (type, extension) => {
+  alarmsMap.set(extension, new Set());
+});
+
+extensions.on("shutdown", (type, extension) => {
+  for (let alarm of alarmsMap.get(extension)) {
+    alarm.clear();
+  }
+  alarmsMap.delete(extension);
+});
+
+extensions.registerAPI((extension, context) => {
+  return {
+    alarms: {
+      create: function(...args) {
+        let name = "", alarmInfo;
+        if (args.length == 1) {
+          alarmInfo = args[0];
+        } else {
+          [name, alarmInfo] = args;
+        }
+
+        let alarm = new Alarm(extension, name, alarmInfo);
+        alarmsMap.get(extension).add(alarm);
+      },
+
+      get: function(args) {
+        let name = "", callback;
+        if (args.length == 1) {
+          callback = args[0];
+        } else {
+          [name, callback] = args;
+        }
+
+        for (let alarm of alarmsMap.get(extension)) {
+          if (alarm.name == name) {
+            runSafe(context, callback, alarm.data);
+            break;
+          }
+        }
+      },
+
+      getAll: function(callback) {
+        let alarms = alarmsMap.get(extension);
+        result = [ for (alarm of alarms) alarm.data ];
+        runSafe(context, callback, result);
+      },
+
+      clear: function(...args) {
+        let name = "", callback;
+        if (args.length == 1) {
+          callback = args[0];
+        } else {
+          [name, callback] = args;
+        }
+
+        let alarms = alarmsMap.get(extension);
+        let cleared = false;
+        for (let alarm of alarms) {
+          if (alarm.name == name) {
+            alarm.clear();
+            cleared = true;
+            break;
+          }
+        }
+
+        if (callback) {
+          runSafe(context, callback, cleared);
+        }
+      },
+
+      clearAll: function(callback) {
+        let alarms = alarmsMap.get(extension);
+        let cleared = false;
+        for (let alarm of alarms) {
+          alarm.clear();
+          cleared = true;
+        }
+        if (callback) {
+          runSafe(context, callback, cleared);
+        }
+      },
+
+      onAlarm: new EventManager(context, "alarms.onAlarm", fire => {
+        let callback = alarm => {
+          fire(alarm.data);
+        };
+
+        alarmCallbacksMap.set(extension, callback);
+        return () => {
+          alarmCallbacksMap.delete(extension);
+        };
+      }).api(),
+    },
+  };
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-backgroundPage.js
@@ -0,0 +1,92 @@
+// WeakMap[Extension -> BackgroundPage]
+let backgroundPagesMap = new WeakMap();
+
+// Responsible for the background_page section of the manifest.
+function BackgroundPage(options, extension)
+{
+  this.extension = extension;
+  this.scripts = options.scripts || [];
+  this.page = options.page || null;
+  this.contentWindow = null;
+  this.webNav = null;
+  this.context = null;
+}
+
+BackgroundPage.prototype = {
+  build() {
+    let webNav = Services.appShell.createWindowlessBrowser(false);
+    this.webNav = webNav;
+
+    let principal = Services.scriptSecurityManager.createCodebasePrincipal(this.extension.baseURI,
+                                                                           {addonId: this.extension.id});
+
+    let interfaceRequestor = webNav.QueryInterface(Ci.nsIInterfaceRequestor);
+    let docShell = interfaceRequestor.getInterface(Ci.nsIDocShell);
+
+    this.context = new ExtensionPage(this.extension, {type: "background", docShell});
+    GlobalManager.injectInDocShell(docShell, this.extension, this.context);
+
+    docShell.createAboutBlankContentViewer(principal);
+
+    let window = webNav.document.defaultView;
+    this.contentWindow = window;
+    this.context.contentWindow = window;
+
+    let url;
+    if (this.page) {
+      url = this.extension.baseURI.resolve(this.page);
+    } else {
+      url = this.extension.baseURI.resolve("_blank.html");
+    }
+    webNav.loadURI(url, 0, null, null, null);
+
+    // TODO: Right now we run onStartup after the background page
+    // finishes. See if this is what Chrome does.
+    window.windowRoot.addEventListener("load", () => {
+      if (this.scripts) {
+        let doc = window.document;
+        for (let script of this.scripts) {
+          let url = this.extension.baseURI.resolve(script);
+          let tag = doc.createElement("script");
+          tag.setAttribute("src", url);
+          tag.async = false;
+          doc.body.appendChild(tag);
+        }
+      }
+
+      if (this.extension.onStartup) {
+        this.extension.onStartup();
+      }
+    }, true);
+  },
+
+  shutdown() {
+    // Navigate away from the background page to invalidate any
+    // setTimeouts or other callbacks.
+    this.webNav.loadURI("about:blank", 0, null, null, null);
+    this.webNav = null;
+  },
+};
+
+extensions.on("manifest_background", (type, directive, extension, manifest) => {
+  let bgPage = new BackgroundPage(manifest.background, extension);
+  bgPage.build();
+  backgroundPagesMap.set(extension, bgPage);
+});
+
+extensions.on("shutdown", (type, extension) => {
+  if (backgroundPagesMap.has(extension)) {
+    backgroundPagesMap.get(extension).shutdown();
+    backgroundPagesMap.delete(extension);
+  }
+});
+
+extensions.registerAPI((extension, context) => {
+  return {
+    extension: {
+      getBackgroundPage: function() {
+        return backgroundPagesMap.get(extension).contentWindow;
+      },
+    },
+  };
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-extension.js
@@ -0,0 +1,10 @@
+extensions.registerAPI((extension, context) => {
+  return {
+    extension: {
+      getURL: function(url) {
+        return extension.baseURI.resolve(url);
+      },
+    },
+  };
+});
+
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-i18n.js
@@ -0,0 +1,9 @@
+extensions.registerAPI((extension, context) => {
+  return {
+    i18n: {
+      getMessage: function(messageName, substitutions) {
+        return extension.localizeMessage(messageName, substitutions);
+      },
+    },
+  };
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-idle.js
@@ -0,0 +1,9 @@
+extensions.registerPrivilegedAPI("idle", (extension, context) => {
+  return {
+    idle: {
+      queryState: function(detectionIntervalInSeconds, callback) {
+        runSafe(context, callback, "active");
+      },
+    },
+  };
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-notifications.js
@@ -0,0 +1,140 @@
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+let {
+  EventManager,
+  ignoreEvent,
+} = ExtensionUtils;
+
+// WeakMap[Extension -> Set[Notification]]
+let notificationsMap = new WeakMap();
+
+// WeakMap[Extension -> callback]
+let notificationCallbacksMap = new WeakMap();
+
+// Manages a notification popup (notifications API) created by the extension.
+function Notification(extension, id, options)
+{
+  this.extension = extension;
+  this.id = id;
+  this.options = options;
+
+  let imageURL;
+  if (options.iconUrl) {
+    imageURL = this.extension.baseURI.resolve(options.iconUrl);
+  }
+
+  try {
+    let svc = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
+    svc.showAlertNotification(imageURL,
+                              options.title,
+                              options.message,
+                              false, // textClickable
+                              this.id,
+                              this,
+                              this.id);
+  } catch (e) {
+    // This will fail if alerts aren't available on the system.
+  }
+}
+
+Notification.prototype = {
+  clear() {
+    try {
+      let svc = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
+      svc.closeAlert(this.id);
+    } catch (e) {
+      // This will fail if the OS doesn't support this function.
+    }
+    notificationsMap.get(this.extension).delete(this);
+  },
+
+  observe(subject, topic, data) {
+    if (topic != "alertfinished") {
+      return;
+    }
+
+    if (notificationCallbacksMap.has(this.extension)) {
+      notificationCallbackMap.get(this.extension)(this);
+    }
+
+    notificationsMap.get(this.extension).delete(this);
+  },
+};
+
+extensions.on("startup", (type, extension) => {
+  notificationsMap.set(extension, new Set());
+});
+
+extensions.on("shutdown", (type, extension) => {
+  for (let notification of notificationsMap.get(extension)) {
+    notification.clear();
+  }
+  notificationsMap.delete(extension);
+});
+
+let nextId = 0;
+
+extensions.registerPrivilegedAPI("notifications", (extension, context) => {
+  return {
+    notifications: {
+      create: function(...args) {
+        let notificationId, options, callback;
+        if (args.length == 1) {
+          options = args[0];
+        } else {
+          [notificationId, options, callback] = args;
+        }
+
+        if (!notificationId) {
+          notificationId = nextId++;
+        }
+
+        // FIXME: Lots of options still aren't supported, especially
+        // buttons.
+        let notification = new Notification(extension, notificationId, options);
+        notificationsMap.get(extension).add(notification);
+
+        if (callback) {
+          runSafe(context, callback, notificationId);
+        }
+      },
+
+      clear: function(notificationId, callback) {
+        let notifications = notificationsMap.get(extension);
+        let cleared = false;
+        for (let notification of notifications) {
+          if (notification.id == notificationId) {
+            notification.clear();
+            cleared = true;
+            break;
+          }
+        }
+
+        if (callback) {
+          runSafe(context, callback, cleared);
+        }
+      },
+
+      getAll: function(callback) {
+        let notifications = notificationsMap.get(extension);
+        notifications = [ for (notification of notifications) notification.id ];
+        runSafe(context, callback, notifications);
+      },
+
+      onClosed: new EventManager(context, "notifications.onClosed", fire => {
+        let listener = notification => {
+          // FIXME: Support the byUser argument.
+          fire(notification.id, true);
+        };
+
+        notificationCallbackMap.set(extension, listener);
+        return () => {
+          notificationCallbackMap.delete(extension);
+        };
+      }).api(),
+
+      // FIXME
+      onButtonClicked: ignoreEvent(),
+      onClicked: ignoreEvent(),
+    },
+  };
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-runtime.js
@@ -0,0 +1,47 @@
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+let {
+  EventManager,
+  ignoreEvent,
+} = ExtensionUtils;
+
+extensions.registerAPI((extension, context) => {
+  return {
+    runtime: {
+      onStartup: new EventManager(context, "runtime.onStartup", fire => {
+        extension.onStartup = fire;
+        return () => {
+          extension.onStartup = null;
+        };
+      }).api(),
+
+      onInstalled: ignoreEvent(),
+
+      onMessage: context.messenger.onMessage("runtime.onMessage"),
+
+      onConnect: context.messenger.onConnect("runtime.onConnect"),
+
+      sendMessage: function(...args) {
+        let extensionId, message, options, responseCallback;
+        if (args.length == 1) {
+          message = args[0];
+        } else if (args.length == 2) {
+          [message, responseCallback] = args;
+        } else {
+          [extensionId, message, options, responseCallback] = args;
+        }
+        let recipient = {extensionId: extensionId ? extensionId : extension.id};
+        return context.messenger.sendMessage(Services.cpmm, message, recipient, responseCallback);
+      },
+
+      getManifest() {
+        return Cu.cloneInto(extension.manifest, context.cloneScope);
+      },
+
+      id: extension.id,
+
+      getURL: function(url) {
+        return extension.baseURI.resolve(url);
+      },
+    },
+  };
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-storage.js
@@ -0,0 +1,48 @@
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
+                                  "resource://gre/modules/ExtensionStorage.jsm");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+let {
+  EventManager,
+  ignoreEvent,
+  runSafe,
+} = ExtensionUtils;
+
+extensions.registerPrivilegedAPI("storage", (extension, context) => {
+  return {
+    storage: {
+      local: {
+        get: function(keys, callback) {
+          ExtensionStorage.get(extension.id, keys).then(result => {
+            runSafe(context, callback, result);
+          });
+        },
+        set: function(items, callback) {
+          ExtensionStorage.set(extension.id, items).then(() => {
+            if (callback) {
+              runSafe(context, callback);
+            }
+          });
+        },
+        remove: function(items, callback) {
+          ExtensionStorage.remove(extension.id, items).then(() => {
+            if (callback) {
+              runSafe(context, callback);
+            }
+          });
+        },
+      },
+
+      onChanged: new EventManager(context, "storage.local.onChanged", fire => {
+        let listener = changes => {
+          fire(changes, "local");
+        };
+
+        ExtensionStorage.addOnChangedListener(extension.id, listener);
+        return () => {
+          ExtensionStorage.removeOnChangedListener(extension.id, listener);
+        };
+      }).api(),
+    },
+  };
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-webNavigation.js
@@ -0,0 +1,70 @@
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
+                                  "resource://gre/modules/ExtensionManagement.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
+                                  "resource://gre/modules/MatchPattern.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WebNavigation",
+                                  "resource://gre/modules/WebNavigation.jsm");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+let {
+  SingletonEventManager,
+  ignoreEvent,
+  runSafe,
+} = ExtensionUtils;
+
+// Similar to WebRequestEventManager but for WebNavigation.
+function WebNavigationEventManager(context, eventName)
+{
+  let name = `webNavigation.${eventName}`;
+  let register = callback => {
+    let listener = data => {
+      if (!data.browser) {
+        return;
+      }
+
+      let tabId = TabManager.getBrowserId(data.browser);
+      if (tabId == -1) {
+        return;
+      }
+
+      let data2 = {
+        url: data.url,
+        timeStamp: Date.now(),
+        frameId: ExtensionManagement.getFrameId(data.windowId),
+        parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId),
+      };
+
+      // Fills in tabId typically.
+      let result = {};
+      extensions.emit("fill-browser-data", data.browser, data2, result);
+      if (result.cancel) {
+        return;
+      }
+
+      return runSafe(context, callback, data2);
+    };
+
+    WebNavigation[eventName].addListener(listener);
+    return () => {
+      WebNavigation[eventName].removeListener(listener);
+    };
+  };
+
+  return SingletonEventManager.call(this, context, name, register);
+}
+
+WebNavigationEventManager.prototype = Object.create(SingletonEventManager.prototype);
+
+extensions.registerPrivilegedAPI("webNavigation", (extension, context) => {
+  return {
+    webNavigation: {
+      onBeforeNavigate: new WebNavigationEventManager(context, "onBeforeNavigate").api(),
+      onCommitted: new WebNavigationEventManager(context, "onCommitted").api(),
+      onDOMContentLoaded: new WebNavigationEventManager(context, "onDOMContentLoaded").api(),
+      onCompleted: new WebNavigationEventManager(context, "onCompleted").api(),
+      onErrorOccurred: new WebNavigationEventManager(context, "onErrorOccurred").api(),
+      onReferenceFragmentUpdated: new WebNavigationEventManager(context, "onReferenceFragmentUpdated").api(),
+      onCreatedNavigationTarget: ignoreEvent(),
+    },
+  };
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-webRequest.js
@@ -0,0 +1,104 @@
+XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
+                                  "resource://gre/modules/MatchPattern.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WebRequest",
+                                  "resource://gre/modules/WebRequest.jsm");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+let {
+  SingletonEventManager,
+  runSafe,
+} = ExtensionUtils;
+
+// EventManager-like class specifically for WebRequest. Inherits from
+// SingletonEventManager. Takes care of converting |details| parameter
+// when invoking listeners.
+function WebRequestEventManager(context, eventName)
+{
+  let name = `webRequest.${eventName}`;
+  let register = (callback, filter, info) => {
+    let listener = data => {
+      if (!data.browser) {
+        return;
+      }
+
+      let tabId = TabManager.getBrowserId(data.browser);
+      if (tabId == -1) {
+        return;
+      }
+
+      let data2 = {
+        url: data.url,
+        method: data.method,
+        type: data.type,
+        timeStamp: Date.now(),
+        frameId: ExtensionManagement.getFrameId(data.windowId),
+        parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId),
+      };
+
+      // Fills in tabId typically.
+      let result = {};
+      extensions.emit("fill-browser-data", data.browser, data2, result);
+      if (result.cancel) {
+        return;
+      }
+
+      let optional = ["requestHeaders", "responseHeaders", "statusCode"];
+      for (let opt of optional) {
+        if (opt in data) {
+          data2[opt] = data[opt];
+        }
+      }
+
+      return runSafe(context, callback, data2);
+    };
+
+    let filter2 = {};
+    filter2.urls = new MatchPattern(filter.urls);
+    if (filter.types) {
+      filter2.types = filter.types;
+    }
+    if (filter.tabId) {
+      filter2.tabId = filter.tabId;
+    }
+    if (filter.windowId) {
+      filter2.windowId = filter.windowId;
+    }
+
+    let info2 = [];
+    if (info) {
+      for (let desc of info) {
+        if (desc == "blocking" && !context.extension.hasPermission("webRequestBlocking")) {
+          Cu.reportError("Using webRequest.addListener with the blocking option " +
+                         "requires the 'webRequestBlocking' permission.");
+        } else {
+          info2.push(desc);
+        }
+      }
+    }
+
+    WebRequest[eventName].addListener(listener, filter2, info2);
+    return () => {
+      WebRequest[eventName].removeListener(listener);
+    };
+  };
+
+  return SingletonEventManager.call(this, context, name, register);
+}
+
+WebRequestEventManager.prototype = Object.create(SingletonEventManager.prototype);
+
+extensions.registerPrivilegedAPI("webRequest", (extension, context) => {
+  return {
+    webRequest: {
+      onBeforeRequest: new WebRequestEventManager(context, "onBeforeRequest").api(),
+      onBeforeSendHeaders: new WebRequestEventManager(context, "onBeforeSendHeaders").api(),
+      onSendHeaders: new WebRequestEventManager(context, "onSendHeaders").api(),
+      onHeadersReceived: new WebRequestEventManager(context, "onHeadersReceived").api(),
+      onResponseStarted: new WebRequestEventManager(context, "onResponseStarted").api(),
+      onCompleted: new WebRequestEventManager(context, "onCompleted").api(),
+      handlerBehaviorChanged: function() {
+        // TODO: Flush all caches.
+      },
+    },
+  };
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/jar.mn
@@ -0,0 +1,16 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+toolkit.jar:
+% content extensions %content/extensions/
+    content/extensions/ext-alarms.js (ext-alarms.js)
+    content/extensions/ext-backgroundPage.js (ext-backgroundPage.js)
+    content/extensions/ext-notifications.js (ext-notifications.js)
+    content/extensions/ext-i18n.js (ext-i18n.js)
+    content/extensions/ext-idle.js (ext-idle.js)
+    content/extensions/ext-webRequest.js (ext-webRequest.js)
+    content/extensions/ext-webNavigation.js (ext-webNavigation.js)
+    content/extensions/ext-runtime.js (ext-runtime.js)
+    content/extensions/ext-extension.js (ext-extension.js)
+    content/extensions/ext-storage.js (ext-storage.js)
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXTRA_JS_MODULES += [
+    'Extension.jsm',
+    'ExtensionContent.jsm',
+    'ExtensionManagement.jsm',
+    'ExtensionStorage.jsm',
+    'ExtensionUtils.jsm',
+]
+
+JAR_MANIFESTS += ['jar.mn']
--- a/toolkit/components/moz.build
+++ b/toolkit/components/moz.build
@@ -17,16 +17,17 @@ DIRS += [
     'asyncshutdown',
     'commandlines',
     'console',
     'contentprefs',
     'cookie',
     'crashmonitor',
     'diskspacewatcher',
     'downloads',
+    'extensions',
     'exthelper',
     'filepicker',
     'filewatcher',
     'finalizationwitness',
     'formautofill',
     'find',
     'gfx',
     'jsdownloads',
--- a/toolkit/components/utils/simpleServices.js
+++ b/toolkit/components/utils/simpleServices.js
@@ -99,17 +99,21 @@ AddonPolicyService.prototype = {
   },
 
   /*
    * Sets the callbacks used in addonMayLoadURI above. Not accessible over
    * XPCOM - callers should use .wrappedJSObject on the service to call it
    * directly.
    */
   setAddonLoadURICallback(aAddonId, aCallback) {
-    this.mayLoadURICallbacks[aAddonId] = aCallback;
+    if (aCallback) {
+      this.mayLoadURICallbacks[aAddonId] = aCallback;
+    } else {
+      delete this.mayLoadURICallbacks[aAddonId];
+    }
   },
 
   /*
    * Sets the callback used in extensionURILoadableByAnyone above. Not
    * accessible over XPCOM - callers should use .wrappedJSObject on the
    * service to call it directly.
    */
   setExtensionURILoadCallback(aCallback) {
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/Locale.jsm
@@ -0,0 +1,93 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+this.EXPORTED_SYMBOLS = ["Locale"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+const PREF_MATCH_OS_LOCALE            = "intl.locale.matchOS";
+const PREF_SELECTED_LOCALE            = "general.useragent.locale";
+
+let Locale = {
+  /**
+   * Gets the currently selected locale for display.
+   * @return  the selected locale or "en-US" if none is selected
+   */
+  getLocale() {
+    if (Preferences.get(PREF_MATCH_OS_LOCALE, false))
+      return Services.locale.getLocaleComponentForUserAgent();
+    try {
+      let locale = Preferences.get(PREF_SELECTED_LOCALE, null, Ci.nsIPrefLocalizedString);
+      if (locale)
+        return locale;
+    }
+    catch (e) {}
+    return Preferences.get(PREF_SELECTED_LOCALE, "en-US");
+  },
+
+  /**
+   * Selects the closest matching locale from a list of locales.
+   *
+   * @param  aLocales
+   *         An array of locales
+   * @return the best match for the currently selected locale
+   */
+  findClosestLocale(aLocales) {
+    let appLocale = this.getLocale();
+
+    // Holds the best matching localized resource
+    var bestmatch = null;
+    // The number of locale parts it matched with
+    var bestmatchcount = 0;
+    // The number of locale parts in the match
+    var bestpartcount = 0;
+
+    var matchLocales = [appLocale.toLowerCase()];
+    /* If the current locale is English then it will find a match if there is
+       a valid match for en-US so no point searching that locale too. */
+    if (matchLocales[0].substring(0, 3) != "en-")
+      matchLocales.push("en-us");
+
+    for (let locale of matchLocales) {
+      var lparts = locale.split("-");
+      for (let localized of aLocales) {
+        for (let found of localized.locales) {
+          found = found.toLowerCase();
+          // Exact match is returned immediately
+          if (locale == found)
+            return localized;
+
+          var fparts = found.split("-");
+          /* If we have found a possible match and this one isn't any longer
+             then we dont need to check further. */
+          if (bestmatch && fparts.length < bestmatchcount)
+            continue;
+
+          // Count the number of parts that match
+          var maxmatchcount = Math.min(fparts.length, lparts.length);
+          var matchcount = 0;
+          while (matchcount < maxmatchcount &&
+                 fparts[matchcount] == lparts[matchcount])
+            matchcount++;
+
+          /* If we matched more than the last best match or matched the same and
+             this locale is less specific than the last best match. */
+          if (matchcount > bestmatchcount ||
+              (matchcount == bestmatchcount && fparts.length < bestpartcount)) {
+            bestmatch = localized;
+            bestmatchcount = matchcount;
+            bestpartcount = fparts.length;
+          }
+        }
+      }
+      // If we found a valid match for this locale return it
+      if (bestmatch)
+        return bestmatch;
+    }
+    return null;
+  },
+};
--- a/toolkit/modules/addons/MatchPattern.jsm
+++ b/toolkit/modules/addons/MatchPattern.jsm
@@ -58,17 +58,17 @@ function SingleMatchPattern(pat)
       Cu.reportError(`Invalid match pattern: '${pat}'`);
       this.scheme = [];
       return;
     }
   }
 }
 
 SingleMatchPattern.prototype = {
-  matches(uri) {
+  matches(uri, ignorePath = false) {
     if (this.scheme.indexOf(uri.scheme) == -1) {
       return false;
     }
 
     // This code ignores the port, as Chrome does.
     if (this.host == '*') {
       // Don't check anything.
     } else if (this.host[0] == '*') {
@@ -78,17 +78,17 @@ SingleMatchPattern.prototype = {
         return false;
       }
     } else {
       if (this.host != uri.host) {
         return false;
       }
     }
 
-    if (!this.path.test(uri.path)) {
+    if (!ignorePath && !this.path.test(uri.path)) {
       return false;
     }
 
     return true;
   }
 };
 
 function MatchPattern(pat)
@@ -109,12 +109,21 @@ MatchPattern.prototype = {
     for (let matcher of this.matchers) {
       if (matcher.matches(uri)) {
         return true;
       }
     }
     return false;
   },
 
+  matchesIgnoringPath(uri) {
+    for (let matcher of this.matchers) {
+      if (matcher.matches(uri, true)) {
+        return true;
+      }
+    }
+    return false;
+  },
+
   serialize() {
     return this.pat;
   },
 };
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -31,16 +31,17 @@ EXTRA_JS_MODULES += [
     'Finder.jsm',
     'Geometry.jsm',
     'GMPInstallManager.jsm',
     'GMPUtils.jsm',
     'Http.jsm',
     'InlineSpellChecker.jsm',
     'InlineSpellCheckerContent.jsm',
     'LoadContextInfo.jsm',
+    'Locale.jsm',
     'Log.jsm',
     'NewTabUtils.jsm',
     'ObjectUtils.jsm',
     'PageMenu.jsm',
     'PageMetadata.jsm',
     'PermissionsUtils.jsm',
     'PopupNotifications.jsm',
     'Preferences.jsm',
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -17,16 +17,18 @@ Components.utils.import("resource://gre/
 Components.utils.import("resource://gre/modules/Preferences.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
                                   "resource://gre/modules/addons/AddonRepository.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ChromeManifestParser",
                                   "resource://gre/modules/ChromeManifestParser.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
                                   "resource://gre/modules/LightweightThemeManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Locale",
+                                  "resource://gre/modules/Locale.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils",
                                   "resource://gre/modules/ZipUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils",
                                   "resource://gre/modules/PermissionsUtils.jsm");
@@ -62,18 +64,16 @@ XPCOMUtils.defineLazyGetter(this, "CertU
 const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
                                        "initWithPath");
 
 const PREF_DB_SCHEMA                  = "extensions.databaseSchema";
 const PREF_INSTALL_CACHE              = "extensions.installCache";
 const PREF_XPI_STATE                  = "extensions.xpiState";
 const PREF_BOOTSTRAP_ADDONS           = "extensions.bootstrappedAddons";
 const PREF_PENDING_OPERATIONS         = "extensions.pendingOperations";
-const PREF_MATCH_OS_LOCALE            = "intl.locale.matchOS";
-const PREF_SELECTED_LOCALE            = "general.useragent.locale";
 const PREF_EM_DSS_ENABLED             = "extensions.dss.enabled";
 const PREF_DSS_SWITCHPENDING          = "extensions.dss.switchPending";
 const PREF_DSS_SKIN_TO_SELECT         = "extensions.lastSelectedSkin";
 const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";
 const PREF_EM_UPDATE_URL              = "extensions.update.url";
 const PREF_EM_UPDATE_BACKGROUND_URL   = "extensions.update.background.url";
 const PREF_EM_ENABLED_ADDONS          = "extensions.enabledAddons";
 const PREF_EM_EXTENSION_FORMAT        = "extensions.";
@@ -510,94 +510,16 @@ SafeInstallOperation.prototype = {
     }
 
     while (this._createdDirs.length > 0)
       recursiveRemove(this._createdDirs.pop());
   }
 };
 
 /**
- * Gets the currently selected locale for display.
- * @return  the selected locale or "en-US" if none is selected
- */
-function getLocale() {
-  if (Preferences.get(PREF_MATCH_OS_LOCALE, false))
-    return Services.locale.getLocaleComponentForUserAgent();
-  try {
-    let locale = Preferences.get(PREF_SELECTED_LOCALE, null, Ci.nsIPrefLocalizedString);
-    if (locale)
-      return locale;
-  }
-  catch (e) {}
-  return Preferences.get(PREF_SELECTED_LOCALE, "en-US");
-}
-
-/**
- * Selects the closest matching locale from a list of locales.
- *
- * @param  aLocales
- *         An array of locales
- * @return the best match for the currently selected locale
- */
-function findClosestLocale(aLocales) {
-  let appLocale = getLocale();
-
-  // Holds the best matching localized resource
-  var bestmatch = null;
-  // The number of locale parts it matched with
-  var bestmatchcount = 0;
-  // The number of locale parts in the match
-  var bestpartcount = 0;
-
-  var matchLocales = [appLocale.toLowerCase()];
-  /* If the current locale is English then it will find a match if there is
-     a valid match for en-US so no point searching that locale too. */
-  if (matchLocales[0].substring(0, 3) != "en-")
-    matchLocales.push("en-us");
-
-  for each (var locale in matchLocales) {
-    var lparts = locale.split("-");
-    for each (var localized in aLocales) {
-      for each (let found in localized.locales) {
-        found = found.toLowerCase();
-        // Exact match is returned immediately
-        if (locale == found)
-          return localized;
-
-        var fparts = found.split("-");
-        /* If we have found a possible match and this one isn't any longer
-           then we dont need to check further. */
-        if (bestmatch && fparts.length < bestmatchcount)
-          continue;
-
-        // Count the number of parts that match
-        var maxmatchcount = Math.min(fparts.length, lparts.length);
-        var matchcount = 0;
-        while (matchcount < maxmatchcount &&
-               fparts[matchcount] == lparts[matchcount])
-          matchcount++;
-
-        /* If we matched more than the last best match or matched the same and
-           this locale is less specific than the last best match. */
-        if (matchcount > bestmatchcount ||
-           (matchcount == bestmatchcount && fparts.length < bestpartcount)) {
-          bestmatch = localized;
-          bestmatchcount = matchcount;
-          bestpartcount = fparts.length;
-        }
-      }
-    }
-    // If we found a valid match for this locale return it
-    if (bestmatch)
-      return bestmatch;
-  }
-  return null;
-}
-
-/**
  * Sets the userDisabled and softDisabled properties of an add-on based on what
  * values those properties had for a previous instance of the add-on. The
  * previous instance may be a previous install or in the case of an application
  * version change the same add-on.
  *
  * NOTE: this may modify aNewAddon in place; callers should save the database if
  * necessary
  *
@@ -6488,17 +6410,17 @@ AddonInternal.prototype = {
   softDisabled: false,
   sourceURI: null,
   releaseNotesURI: null,
   foreignInstall: false,
 
   get selectedLocale() {
     if (this._selectedLocale)
       return this._selectedLocale;
-    let locale = findClosestLocale(this.locales);
+    let locale = Locale.findClosestLocale(this.locales);
     this._selectedLocale = locale ? locale : this.defaultLocale;
     return this._selectedLocale;
   },
 
   get providesUpdatesSecurely() {
     return !!(this.updateKey || !this.updateURL ||
               this.updateURL.substring(0, 6) == "https:");
   },