Bug 1234677 - Introduce _generated_background_page.html r=billm
☠☠ backed out by 77e960d65f64 ☠ ☠
authorRob Wu <rob@robwu.nl>
Tue, 12 Jul 2016 13:55:14 -0700
changeset 330603 4dd2466573eceb92c70f8436ad54bb400ae4110b
parent 330602 e8e2132fa883f916ed763f890c9cbdfddbf8cdf1
child 330604 99837fd9225866b6416caa2f7cd97e9632bad0ea
push id9858
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 14:37:10 +0000
treeherdermozilla-aurora@203106ef6cb6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbillm
bugs1234677, 1286057
milestone50.0a1
Bug 1234677 - Introduce _generated_background_page.html r=billm - Fixes bugzil.la/1234677 - Fixes bugzil.la/1286057 - Fixes bug: the URL failed to load if a query string or reference fragment was present. MozReview-Commit-ID: 4oMwI3IS7OX
browser/components/extensions/test/browser/browser_ext_currentWindow.js
caps/nsIAddonPolicyService.idl
netwerk/protocol/res/ExtensionProtocolHandler.cpp
netwerk/protocol/res/ExtensionProtocolHandler.h
netwerk/protocol/res/SubstitutingProtocolHandler.cpp
netwerk/protocol/res/SubstitutingProtocolHandler.h
netwerk/protocol/res/nsResProtocolHandler.cpp
netwerk/protocol/res/nsResProtocolHandler.h
toolkit/components/extensions/ExtensionManagement.jsm
toolkit/components/extensions/ext-backgroundPage.js
toolkit/components/extensions/test/mochitest/mochitest.ini
toolkit/components/extensions/test/mochitest/test_ext_background_generated_load_events.html
toolkit/components/extensions/test/mochitest/test_ext_background_generated_reload.html
toolkit/components/extensions/test/mochitest/test_ext_background_generated_url.html
toolkit/components/utils/simpleServices.js
--- a/browser/components/extensions/test/browser/browser_ext_currentWindow.js
+++ b/browser/components/extensions/test/browser/browser_ext_currentWindow.js
@@ -1,18 +1,18 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 function genericChecker() {
   let kind = "background";
   let path = window.location.pathname;
-  if (path.indexOf("popup") != -1) {
+  if (path.includes("/popup.html")) {
     kind = "popup";
-  } else if (path.indexOf("page") != -1) {
+  } else if (path.includes("/page.html")) {
     kind = "page";
   }
 
   browser.test.onMessage.addListener((msg, ...args) => {
     if (msg == kind + "-check-current1") {
       browser.tabs.query({
         currentWindow: true,
       }, function(tabs) {
@@ -29,17 +29,17 @@ function genericChecker() {
         browser.test.sendMessage("result", window.id);
       });
     } else if (msg == kind + "-open-page") {
       browser.tabs.create({windowId: args[0], url: browser.runtime.getURL("page.html")});
     } else if (msg == kind + "-close-page") {
       browser.tabs.query({
         windowId: args[0],
       }, tabs => {
-        let tab = tabs.find(tab => tab.url.indexOf("page.html") != -1);
+        let tab = tabs.find(tab => tab.url.includes("/page.html"));
         browser.tabs.remove(tab.id, () => {
           browser.test.sendMessage("closed");
         });
       });
     }
   });
   browser.test.sendMessage(kind + "-ready");
 }
--- a/caps/nsIAddonPolicyService.idl
+++ b/caps/nsIAddonPolicyService.idl
@@ -29,16 +29,23 @@ interface nsIAddonPolicyService : nsISup
   /**
    * Returns the content security policy which applies to documents belonging
    * to the extension with the given ID. This may be either a custom policy,
    * if one was supplied, or the default policy if one was not.
    */
   AString getAddonCSP(in AString aAddonId);
 
   /**
+   * Returns the generated background page as a data-URI, if any. If the addon
+   * does not have an auto-generated background page, an empty string is
+   * returned.
+   */
+  ACString getGeneratedBackgroundPageUrl(in ACString aAddonId);
+
+  /**
    * Returns true if unprivileged code associated with the given addon may load
    * data from |aURI|.
    */
   boolean addonMayLoadURI(in AString aAddonId, in nsIURI aURI);
 
   /**
    * Returns true if a given extension:// URI is web-accessible.
    */
--- a/netwerk/protocol/res/ExtensionProtocolHandler.cpp
+++ b/netwerk/protocol/res/ExtensionProtocolHandler.cpp
@@ -73,16 +73,49 @@ protected:
   virtual ~PipeCloser() {}
 
 private:
   nsCOMPtr<nsIOutputStream> mOutputStream;
 };
 
 NS_IMPL_ISUPPORTS(PipeCloser, nsIRequestObserver)
 
+bool
+ExtensionProtocolHandler::ResolveSpecialCases(const nsACString& aHost,
+                                              const nsACString& aPath,
+                                              const nsACString& aPathname,
+                                              nsACString& aResult)
+{
+  // Create special moz-extension:-pages such as moz-extension://foo/_blank.html
+  // for all registered extensions. We can't just do this as a substitution
+  // because substitutions can only match on host.
+  if (!SubstitutingProtocolHandler::HasSubstitution(aHost)) {
+    return false;
+  }
+  if (aPathname.EqualsLiteral("/_blank.html")) {
+    aResult.AssignLiteral("about:blank");
+    return true;
+  }
+  if (aPathname.EqualsLiteral("/_generated_background_page.html")) {
+    nsCOMPtr<nsIAddonPolicyService> aps =
+      do_GetService("@mozilla.org/addons/policy-service;1");
+    if (!aps) {
+      return false;
+    }
+    nsresult rv = aps->GetGeneratedBackgroundPageUrl(aHost, aResult);
+    NS_ENSURE_SUCCESS(rv, false);
+    if (!aResult.IsEmpty()) {
+      MOZ_RELEASE_ASSERT(Substring(aResult, 0, 5).Equals("data:"));
+      return true;
+    }
+  }
+
+  return false;
+}
+
 nsresult
 ExtensionProtocolHandler::SubstituteChannel(nsIURI* aURI,
                                             nsILoadInfo* aLoadInfo,
                                             nsIChannel** result)
 {
   nsresult rv;
   nsCOMPtr<nsIURL> url = do_QueryInterface(aURI, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
--- a/netwerk/protocol/res/ExtensionProtocolHandler.h
+++ b/netwerk/protocol/res/ExtensionProtocolHandler.h
@@ -23,28 +23,20 @@ public:
   NS_FORWARD_NSIPROTOCOLHANDLER(SubstitutingProtocolHandler::)
   NS_FORWARD_NSISUBSTITUTINGPROTOCOLHANDLER(SubstitutingProtocolHandler::)
 
   ExtensionProtocolHandler() : SubstitutingProtocolHandler("moz-extension") {}
 
 protected:
   ~ExtensionProtocolHandler() {}
 
-  bool ResolveSpecialCases(const nsACString& aHost, const nsACString& aPath, nsACString& aResult) override
-  {
-    // Create a special about:blank-like moz-extension://foo/_blank.html for all
-    // registered extensions. We can't just do this as a substitution because
-    // substitutions can only match on host.
-    if (SubstitutingProtocolHandler::HasSubstitution(aHost) && aPath.EqualsLiteral("/_blank.html")) {
-      aResult.AssignLiteral("about:blank");
-      return true;
-    }
-
-    return false;
-  }
+  bool ResolveSpecialCases(const nsACString& aHost,
+                           const nsACString& aPath,
+                           const nsACString& aPathname,
+                           nsACString& aResult) override;
 
   virtual nsresult SubstituteChannel(nsIURI* uri, nsILoadInfo* aLoadInfo, nsIChannel** result) override;
 };
 
 } // namespace net
 } // namespace mozilla
 
 #endif /* ExtensionProtocolHandler_h___ */
--- a/netwerk/protocol/res/SubstitutingProtocolHandler.cpp
+++ b/netwerk/protocol/res/SubstitutingProtocolHandler.cpp
@@ -338,43 +338,43 @@ SubstitutingProtocolHandler::HasSubstitu
 
 nsresult
 SubstitutingProtocolHandler::ResolveURI(nsIURI *uri, nsACString &result)
 {
   nsresult rv;
 
   nsAutoCString host;
   nsAutoCString path;
+  nsAutoCString pathname;
+
+  nsCOMPtr<nsIURL> url = do_QueryInterface(uri);
+  if (!url) {
+    return NS_ERROR_MALFORMED_URI;
+  }
 
   rv = uri->GetAsciiHost(host);
   if (NS_FAILED(rv)) return rv;
 
   rv = uri->GetPath(path);
   if (NS_FAILED(rv)) return rv;
 
-  if (ResolveSpecialCases(host, path, result)) {
+  rv = url->GetFilePath(pathname);
+  if (NS_FAILED(rv)) return rv;
+
+  if (ResolveSpecialCases(host, path, pathname, result)) {
     return NS_OK;
   }
 
   nsCOMPtr<nsIURI> baseURI;
   rv = GetSubstitution(host, getter_AddRefs(baseURI));
   if (NS_FAILED(rv)) return rv;
 
   // Unescape the path so we can perform some checks on it.
-  nsCOMPtr<nsIURL> url = do_QueryInterface(uri);
-  if (!url) {
-    return NS_ERROR_MALFORMED_URI;
-  }
-
-  nsAutoCString unescapedPath;
-  rv = url->GetFilePath(unescapedPath);
-  if (NS_FAILED(rv)) return rv;
-
-  NS_UnescapeURL(unescapedPath);
-  if (unescapedPath.FindChar('\\') != -1) {
+  NS_UnescapeURL(pathname);
+  if (pathname.FindChar('\\') != -1) {
     return NS_ERROR_MALFORMED_URI;
   }
 
   // Some code relies on an empty path resolving to a file rather than a
   // directory.
   NS_ASSERTION(path.CharAt(0) == '/', "Path must begin with '/'");
   if (path.Length() == 1) {
     rv = baseURI->GetSpec(result);
--- a/netwerk/protocol/res/SubstitutingProtocolHandler.h
+++ b/netwerk/protocol/res/SubstitutingProtocolHandler.h
@@ -50,17 +50,20 @@ protected:
   virtual nsresult GetSubstitutionInternal(const nsACString& aRoot, nsIURI** aResult)
   {
     *aResult = nullptr;
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   // Override this in the subclass to check for special case when resolving URIs
   // _before_ checking substitutions.
-  virtual bool ResolveSpecialCases(const nsACString& aHost, const nsACString& aPath, nsACString& aResult)
+  virtual bool ResolveSpecialCases(const nsACString& aHost,
+                                   const nsACString& aPath,
+                                   const nsACString& aPathname,
+                                   nsACString& aResult)
   {
     return false;
   }
 
   // Override this in the subclass to check for special case when opening
   // channels.
   virtual nsresult SubstituteChannel(nsIURI* uri, nsILoadInfo* aLoadInfo, nsIChannel** result)
   {
--- a/netwerk/protocol/res/nsResProtocolHandler.cpp
+++ b/netwerk/protocol/res/nsResProtocolHandler.cpp
@@ -61,26 +61,27 @@ NS_IMPL_QUERY_INTERFACE(nsResProtocolHan
 NS_IMPL_ADDREF_INHERITED(nsResProtocolHandler, SubstitutingProtocolHandler)
 NS_IMPL_RELEASE_INHERITED(nsResProtocolHandler, SubstitutingProtocolHandler)
 
 nsresult
 nsResProtocolHandler::GetSubstitutionInternal(const nsACString& root, nsIURI **result)
 {
     nsAutoCString uri;
 
-    if (!ResolveSpecialCases(root, NS_LITERAL_CSTRING("/"), uri)) {
+    if (!ResolveSpecialCases(root, NS_LITERAL_CSTRING("/"), NS_LITERAL_CSTRING("/"), uri)) {
         return NS_ERROR_NOT_AVAILABLE;
     }
 
     return NS_NewURI(result, uri);
 }
 
 bool
 nsResProtocolHandler::ResolveSpecialCases(const nsACString& aHost,
                                           const nsACString& aPath,
+                                          const nsACString& aPathname,
                                           nsACString& aResult)
 {
     if (aHost.Equals("") || aHost.Equals(kAPP)) {
         aResult.Assign(mAppURI);
     } else if (aHost.Equals(kGRE)) {
         aResult.Assign(mGREURI);
     } else {
         return false;
--- a/netwerk/protocol/res/nsResProtocolHandler.h
+++ b/netwerk/protocol/res/nsResProtocolHandler.h
@@ -47,17 +47,19 @@ public:
     {
         return mozilla::SubstitutingProtocolHandler::ResolveURI(aResURI, aResult);
     }
 
 protected:
     nsresult GetSubstitutionInternal(const nsACString& aRoot, nsIURI** aResult) override;
     virtual ~nsResProtocolHandler() {}
 
-    bool ResolveSpecialCases(const nsACString& aHost, const nsACString& aPath,
+    bool ResolveSpecialCases(const nsACString& aHost,
+                             const nsACString& aPath,
+                             const nsACString& aPathname,
                              nsACString& aResult) override;
 
 private:
     nsCString mAppURI;
     nsCString mGREURI;
 };
 
 #endif /* nsResProtocolHandler_h___ */
--- a/toolkit/components/extensions/ExtensionManagement.jsm
+++ b/toolkit/components/extensions/ExtensionManagement.jsm
@@ -156,25 +156,27 @@ var Service = {
     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));
     this.aps.setAddonLocalizeCallback(extension.id, extension.localize.bind(extension));
     this.aps.setAddonCSP(extension.id, extension.manifest.content_security_policy);
+    this.aps.setBackgroundPageUrlCallback(uuid, this.generateBackgroundPageUrl.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);
     this.aps.setAddonLocalizeCallback(extension.id, null);
     this.aps.setAddonCSP(extension.id, null);
+    this.aps.setBackgroundPageUrlCallback(uuid, 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
@@ -195,16 +197,31 @@ var Service = {
 
   // 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);
   },
 
+  generateBackgroundPageUrl(extension) {
+    let background_scripts = extension.manifest.background &&
+      extension.manifest.background.scripts;
+    if (!background_scripts) {
+      return;
+    }
+    let html = "<!DOCTYPE html>\n<body>\n";
+    for (let script of background_scripts) {
+      script = script.replace(/"/g, "&quot;");
+      html += `<script src="${script}"></script>\n`;
+    }
+    html += "</body>\n</html>\n";
+    return "data:text/html;charset=utf-8," + encodeURIComponent(html);
+  },
+
   // 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) {
     let uuid = uri.host;
     let extension = this.uuidMap.get(uuid);
     return extension ? extension.id : undefined;
   },
--- a/toolkit/components/extensions/ext-backgroundPage.js
+++ b/toolkit/components/extensions/ext-backgroundPage.js
@@ -7,35 +7,34 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/AddonManager.jsm");
 
 // WeakMap[Extension -> BackgroundPage]
 var 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.isGenerated = !!options.scripts;
   this.contentWindow = null;
   this.chromeWebNav = null;
   this.webNav = null;
   this.context = null;
 }
 
 BackgroundPage.prototype = {
   build() {
     let chromeWebNav = Services.appShell.createWindowlessBrowser(true);
     this.chromeWebNav = chromeWebNav;
 
     let url;
     if (this.page) {
       url = this.extension.baseURI.resolve(this.page);
-    } else {
-      // TODO: Chrome uses "_generated_background_page.html" for this.
-      url = this.extension.baseURI.resolve("_blank.html");
+    } else if (this.isGenerated) {
+      url = this.extension.baseURI.resolve("_generated_background_page.html");
     }
 
     if (!this.extension.isExtensionURL(url)) {
       this.extension.manifestError("Background page must be a file within the extension");
       url = this.extension.baseURI.resolve("_blank.html");
     }
 
     let system = Services.scriptSecurityManager.getSystemPrincipal();
@@ -98,26 +97,16 @@ BackgroundPage.prototype = {
       Components.utils.exportFunction(alertOverwrite, window, {
         defineAs: "alert",
       });
       if (event.target != window.document) {
         return;
       }
       event.currentTarget.removeEventListener("load", loadListener, true);
 
-      if (this.scripts) {
-        let doc = window.document;
-        for (let script of this.scripts) {
-          let tag = doc.createElement("script");
-          tag.setAttribute("src", script);
-          tag.async = false;
-          doc.body.appendChild(tag);
-        }
-      }
-
       if (this.extension.onStartup) {
         this.extension.onStartup();
       }
     };
     browser.addEventListener("load", loadListener, true);
   },
 
   shutdown() {
--- a/toolkit/components/extensions/test/mochitest/mochitest.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest.ini
@@ -82,16 +82,19 @@ skip-if = os == 'android' # Android does
 [test_ext_background_runtime_connect_params.html]
 [test_ext_cookies.html]
 [test_ext_bookmarks.html]
 skip-if = (os == 'android' || buildapp == 'b2g') # unimplemented api. Bug 1258975 on android.
 [test_ext_alarms.html]
 [test_ext_background_window_properties.html]
 [test_ext_background_sub_windows.html]
 [test_ext_background_api_injection.html]
+[test_ext_background_generated_url.html]
+[test_ext_background_generated_reload.html]
+[test_ext_background_generated_load_events.html]
 [test_ext_i18n.html]
 skip-if = (os == 'android') # Bug 1258975 on android.
 [test_ext_web_accessible_resources.html]
 skip-if = (os == 'android') # Bug 1258975 on android.
 [test_ext_webrequest.html]
 skip-if = (os == 'android' || buildapp == 'b2g') # webrequest api uninplemented (bug 1199504). Bug 1258975 on android.
 [test_ext_webnavigation.html]
 skip-if = (os == 'android' || buildapp == 'b2g') # needs TabManager which is not yet implemented. Bug 1258975 on android.
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_background_generated_load_events.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test load events in _generated_background_page.html</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script>
+"use strict";
+
+/* eslint-disable mozilla/balanced-listeners */
+
+add_task(function* test_DOMContentLoaded_in_generated_background_page() {
+  function backgroundScript() {
+    function reportListener(event) {
+      browser.test.sendMessage("eventname", event.type);
+    }
+    document.addEventListener("DOMContentLoaded", reportListener);
+    window.addEventListener("load", reportListener);
+  }
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      background: {
+        scripts: ["bg.js"],
+      },
+      web_accessible_resources: ["_generated_background_page.html"],
+    },
+    files: {
+      "bg.js": `(${backgroundScript})();`,
+    },
+  });
+
+  yield extension.startup();
+  is("DOMContentLoaded", yield extension.awaitMessage("eventname"));
+  is("load", yield extension.awaitMessage("eventname"));
+
+  yield extension.unload();
+});
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_background_generated_reload.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test reload of _generated_background_page.html</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script>
+"use strict";
+
+add_task(function* test_reload_generated_background_page() {
+  function backgroundScript() {
+    if (location.hash !== "#firstrun") {
+      browser.test.sendMessage("first run");
+      location.hash = "#firstrun";
+      browser.test.assertEq("#firstrun", location.hash);
+      location.reload();
+    } else {
+      browser.test.notifyPass("second run");
+    }
+  }
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      background: {
+        scripts: ["bg.js"],
+      },
+    },
+    files: {
+      "bg.js": `(${backgroundScript})();`,
+    },
+  });
+
+  yield extension.startup();
+  info("Waiting for first message");
+  yield extension.awaitMessage("first run");
+  info("Waiting for second message");
+  yield extension.awaitFinish("second run");
+  info("Received both messages");
+
+  yield extension.unload();
+});
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_background_generated_url.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test _generated_background_page.html</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script>
+"use strict";
+
+add_task(function* test_url_of_generated_background_page() {
+  function backgroundScript() {
+    const EXPECTED_URL = browser.runtime.getURL("/_generated_background_page.html");
+    browser.test.assertEq(EXPECTED_URL, location.href);
+    browser.test.sendMessage("script done", EXPECTED_URL);
+  }
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      background: {
+        scripts: ["bg.js"],
+      },
+      web_accessible_resources: ["_generated_background_page.html"],
+    },
+    files: {
+      "bg.js": `(${backgroundScript})();`,
+    },
+  });
+
+  yield extension.startup();
+  const EXPECTED_URL = yield extension.awaitMessage("script done");
+
+  let win = window.open(EXPECTED_URL);
+  ok(win, "Should open new tab at URL: " + EXPECTED_URL);
+  yield extension.awaitMessage("script done");
+  win.close();
+
+  yield extension.unload();
+});
+
+</script>
+</body>
+</html>
--- a/toolkit/components/utils/simpleServices.js
+++ b/toolkit/components/utils/simpleServices.js
@@ -23,16 +23,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
 function AddonPolicyService()
 {
   this.wrappedJSObject = this;
   this.cspStrings = new Map();
+  this.backgroundPageUrlCallbacks = new Map();
   this.mayLoadURICallbacks = new Map();
   this.localizeCallbacks = new Map();
 
   XPCOMUtils.defineLazyPreferenceGetter(
     this, "baseCSP", "extensions.webextensions.base-content-security-policy",
     "script-src 'self' https://* moz-extension: blob: filesystem: 'unsafe-eval' 'unsafe-inline'; " +
     "object-src 'self' https://* moz-extension: blob: filesystem:;");
 
@@ -50,16 +51,26 @@ AddonPolicyService.prototype = {
    * to the extension with the given ID. This may be either a custom policy,
    * if one was supplied, or the default policy if one was not.
    */
   getAddonCSP(aAddonId) {
     let csp = this.cspStrings.get(aAddonId);
     return csp || this.defaultCSP;
   },
 
+  /**
+   * Returns the generated background page as a data-URI, if any. If the addon
+   * does not have an auto-generated background page, an empty string is
+   * returned.
+   */
+  getGeneratedBackgroundPageUrl(aAddonId) {
+    let cb = this.backgroundPageUrlCallbacks.get(aAddonId);
+    return cb && cb(aAddonId) || '';
+  },
+
   /*
    * Invokes a callback (if any) associated with the addon to determine whether
    * unprivileged code running within the addon is allowed to perform loads from
    * the given URI.
    *
    * @see nsIAddonPolicyService.addonMayLoadURI
    */
   addonMayLoadURI(aAddonId, aURI) {
@@ -129,16 +140,27 @@ AddonPolicyService.prototype = {
   setAddonCSP(aAddonId, aCSPString) {
     if (aCSPString) {
       this.cspStrings.set(aAddonId, aCSPString);
     } else {
       this.cspStrings.delete(aAddonId);
     }
   },
 
+  /**
+   * Set the callback that generates a data-URL for the background page.
+   */
+  setBackgroundPageUrlCallback(aAddonId, aCallback) {
+    if (aCallback) {
+      this.backgroundPageUrlCallbacks.set(aAddonId, aCallback);
+    } else {
+      this.backgroundPageUrlCallbacks.delete(aAddonId);
+    }
+  },
+
   /*
    * Sets the callbacks used by the stream converter service to localize
    * add-on resources.
    */
   setAddonLocalizeCallback(aAddonId, aCallback) {
     if (aCallback) {
       this.localizeCallbacks.set(aAddonId, aCallback);
     } else {