Bug 1234677 - Introduce _generated_background_page.html r=billm
authorRob Wu <rob@robwu.nl>
Tue, 12 Jul 2016 13:55:14 -0700
changeset 306047 36b524e69dc5250a87d8f6c42a369c817d508f39
parent 306046 58a8eeabbe7f4cb01e895c90bb6a16df01339e4a
child 306048 b30a96d890b60d296ec1830f0743ee03716971ed
push id79765
push usercbook@mozilla.com
push dateThu, 21 Jul 2016 14:26:34 +0000
treeherdermozilla-inbound@ab54bfc55266 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbillm
bugs1234677, 1286057
milestone50.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 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();
@@ -73,26 +72,16 @@ BackgroundPage.prototype = {
     // TODO(robwu): This implementation of onStartup is wrong, see
     // https://bugzilla.mozilla.org/show_bug.cgi?id=1247435#c1
     let loadListener = event => {
       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 {