Bug 1234677 - Introduce _generated_background_page.html
- 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
--- 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, """);
+ 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
@@ -80,16 +80,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 {