diff --git a/browser/components/translation/BingTranslator.jsm b/browser/components/translation/BingTranslator.jsm
index 3d016fb5706a..6442a1d8ff35 100644
--- a/browser/components/translation/BingTranslator.jsm
+++ b/browser/components/translation/BingTranslator.jsm
@@ -6,7 +6,7 @@
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-this.EXPORTED_SYMBOLS = [ "BingTranslation" ];
+this.EXPORTED_SYMBOLS = [ "BingTranslator" ];
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Log.jsm");
@@ -40,7 +40,7 @@ const MAX_REQUESTS = 15;
* @returns {Promise} A promise that will resolve when the translation
* task is finished.
*/
-this.BingTranslation = function(translationDocument, sourceLanguage, targetLanguage) {
+this.BingTranslator = function(translationDocument, sourceLanguage, targetLanguage) {
this.translationDocument = translationDocument;
this.sourceLanguage = sourceLanguage;
this.targetLanguage = targetLanguage;
@@ -50,7 +50,7 @@ this.BingTranslation = function(translationDocument, sourceLanguage, targetLangu
this._translatedCharacterCount = 0;
};
-this.BingTranslation.prototype = {
+this.BingTranslator.prototype = {
/**
* Performs the translation, splitting the document into several chunks
* respecting the data limits of the API.
@@ -282,7 +282,10 @@ BingRequest.prototype = {
return Task.spawn(function *(){
let token = yield BingTokenManager.getToken();
let auth = "Bearer " + token;
- let request = new RESTRequest("https://api.microsofttranslator.com/v2/Http.svc/TranslateArray");
+ let url = getUrlParam("https://api.microsofttranslator.com/v2/Http.svc/TranslateArray",
+ "browser.translation.bing.translateArrayURL",
+ false);
+ let request = new RESTRequest(url);
request.setHeader("Content-type", "text/xml");
request.setHeader("Authorization", auth);
@@ -358,15 +361,18 @@ let BingTokenManager = {
* string once it is obtained.
*/
_getNewToken: function() {
- let request = new RESTRequest("https://datamarket.accesscontrol.windows.net/v2/OAuth2-13");
+ let url = getUrlParam("https://datamarket.accesscontrol.windows.net/v2/OAuth2-13",
+ "browser.translation.bing.authURL",
+ false);
+ let request = new RESTRequest(url);
request.setHeader("Content-type", "application/x-www-form-urlencoded");
let params = [
"grant_type=client_credentials",
"scope=" + encodeURIComponent("http://api.microsofttranslator.com"),
"client_id=" +
- getAuthTokenParam("%BING_API_CLIENTID%", "browser.translation.bing.clientIdOverride"),
+ getUrlParam("%BING_API_CLIENTID%", "browser.translation.bing.clientIdOverride"),
"client_secret=" +
- getAuthTokenParam("%BING_API_KEY%", "browser.translation.bing.apiKeyOverride")
+ getUrlParam("%BING_API_KEY%", "browser.translation.bing.apiKeyOverride")
];
let deferred = Promise.defer();
@@ -416,11 +422,10 @@ function escapeXML(aStr) {
* Fetch an auth token (clientID or client secret), which may be overridden by
* a pref if it's set.
*/
-function getAuthTokenParam(key, prefName) {
- let val;
- try {
- val = Services.prefs.getCharPref(prefName);
- } catch(ex) {}
+function getUrlParam(paramValue, prefName, encode = true) {
+ if (Services.prefs.getPrefType(prefName))
+ paramValue = Services.prefs.getCharPref(prefName);
+ paramValue = Services.urlFormatter.formatURL(paramValue);
- return encodeURIComponent(Services.urlFormatter.formatURL(val || key));
+ return encode ? encodeURIComponent(paramValue) : paramValue;
}
diff --git a/browser/components/translation/Translation.jsm b/browser/components/translation/Translation.jsm
index 608b3fdc4aa0..c2029b95a254 100644
--- a/browser/components/translation/Translation.jsm
+++ b/browser/components/translation/Translation.jsm
@@ -93,10 +93,16 @@ this.Translation = {
*/
function TranslationUI(aBrowser) {
this.browser = aBrowser;
- aBrowser.messageManager.addMessageListener("Translation:Finished", this);
}
TranslationUI.prototype = {
+ get browser() this._browser,
+ set browser(aBrowser) {
+ if (this._browser)
+ this._browser.messageManager.removeMessageListener("Translation:Finished", this);
+ aBrowser.messageManager.addMessageListener("Translation:Finished", this);
+ this._browser = aBrowser;
+ },
translate: function(aFrom, aTo) {
if (aFrom == aTo ||
(this.state == Translation.STATE_TRANSLATED &&
@@ -124,7 +130,17 @@ TranslationUI.prototype = {
if (notification)
PopupNotifications.remove(notification);
- let callback = aTopic => {
+ let callback = (aTopic, aNewBrowser) => {
+ if (aTopic == "swapping") {
+ let infoBarVisible =
+ this.notificationBox.getNotificationWithValue("translation");
+ aNewBrowser.translationUI = this;
+ this.browser = aNewBrowser;
+ if (infoBarVisible)
+ this.showTranslationInfoBar();
+ return true;
+ }
+
if (aTopic != "showing")
return false;
let notification = this.notificationBox.getNotificationWithValue("translation");
diff --git a/browser/components/translation/TranslationContentHandler.jsm b/browser/components/translation/TranslationContentHandler.jsm
index 41c04ed25941..773cfcf253c3 100644
--- a/browser/components/translation/TranslationContentHandler.jsm
+++ b/browser/components/translation/TranslationContentHandler.jsm
@@ -121,16 +121,16 @@ TranslationContentHandler.prototype = {
// translated text.
let translationDocument = this.global.content.translationDocument ||
new TranslationDocument(this.global.content.document);
- let bingTranslation = new BingTranslation(translationDocument,
- msg.data.from,
- msg.data.to);
+ let bingTranslator = new BingTranslator(translationDocument,
+ msg.data.from,
+ msg.data.to);
this.global.content.translationDocument = translationDocument;
translationDocument.translatedFrom = msg.data.from;
translationDocument.translatedTo = msg.data.to;
translationDocument.translationError = false;
- bingTranslation.translate().then(
+ bingTranslator.translate().then(
result => {
this.global.sendAsyncMessage("Translation:Finished", {
characterCount: result.characterCount,
diff --git a/browser/components/translation/test/bing.sjs b/browser/components/translation/test/bing.sjs
new file mode 100644
index 000000000000..81bd6732b2c3
--- /dev/null
+++ b/browser/components/translation/test/bing.sjs
@@ -0,0 +1,218 @@
+/* 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 {classes: Cc, interfaces: Ci, Constructor: CC} = Components;
+const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream");
+
+function handleRequest(req, res) {
+ try {
+ reallyHandleRequest(req, res);
+ } catch (ex) {
+ res.setStatusLine("1.0", 200, "AlmostOK");
+ let msg = "Error handling request: " + ex + "\n" + ex.stack;
+ log(msg);
+ res.write(msg);
+ }
+}
+
+function log(msg) {
+ // dump("BING-SERVER-MOCK: " + msg + "\n");
+}
+
+const statusCodes = {
+ 400: "Bad Request",
+ 401: "Unauthorized",
+ 403: "Forbidden",
+ 404: "Not Found",
+ 405: "Method Not Allowed",
+ 500: "Internal Server Error",
+ 501: "Not Implemented",
+ 503: "Service Unavailable"
+};
+
+function HTTPError(code = 500, message) {
+ this.code = code;
+ this.name = statusCodes[code] || "HTTPError";
+ this.message = message || this.name;
+}
+HTTPError.prototype = new Error();
+HTTPError.prototype.constructor = HTTPError;
+
+function sendError(res, err) {
+ if (!(err instanceof HTTPError)) {
+ err = new HTTPError(typeof err == "number" ? err : 500,
+ err.message || typeof err == "string" ? err : "");
+ }
+ res.setStatusLine("1.1", err.code, err.name);
+ res.write(err.message);
+}
+
+function parseQuery(query) {
+ let ret = {};
+ for (let param of query.replace(/^[?&]/, "").split("&")) {
+ param = param.split("=");
+ if (!param[0])
+ continue;
+ ret[unescape(param[0])] = unescape(param[1]);
+ }
+ return ret;
+}
+
+function getRequestBody(req) {
+ let avail;
+ let bytes = [];
+ let body = new BinaryInputStream(req.bodyInputStream);
+
+ while ((avail = body.available()) > 0)
+ Array.prototype.push.apply(bytes, body.readByteArray(avail));
+
+ return String.fromCharCode.apply(null, bytes);
+}
+
+function sha1(str) {
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ // `result` is an out parameter, `result.value` will contain the array length.
+ let result = {};
+ // `data` is an array of bytes.
+ let data = converter.convertToByteArray(str, result);
+ let ch = Cc["@mozilla.org/security/hash;1"]
+ .createInstance(Ci.nsICryptoHash);
+ ch.init(ch.SHA1);
+ ch.update(data, data.length);
+ let hash = ch.finish(false);
+
+ // Return the two-digit hexadecimal code for a byte.
+ function toHexString(charCode) {
+ return ("0" + charCode.toString(16)).slice(-2);
+ }
+
+ // Convert the binary hash data to a hex string.
+ return [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
+}
+
+function parseXml(body) {
+ let DOMParser = Cc["@mozilla.org/xmlextras/domparser;1"]
+ .createInstance(Ci.nsIDOMParser);
+ let xml = DOMParser.parseFromString(body, "text/xml");
+ if (xml.documentElement.localName == "parsererror")
+ throw new Error("Invalid XML");
+ return xml;
+}
+
+function getInputStream(path) {
+ let file = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsILocalFile);
+ for (let part of path.split("/"))
+ file.append(part);
+ let fileStream = Cc["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Ci.nsIFileInputStream);
+ fileStream.init(file, 1, 0, false);
+ return fileStream;
+}
+
+function checkAuth(req) {
+ let err = new Error("Authorization failed");
+ err.code = 401;
+
+ if (!req.hasHeader("Authorization"))
+ throw new HTTPError(401, "No Authorization header provided.");
+
+ let auth = req.getHeader("Authorization");
+ if (!auth.startsWith("Bearer "))
+ throw new HTTPError(401, "Invalid Authorization header content: '" + auth + "'");
+}
+
+function reallyHandleRequest(req, res) {
+ log("method: " + req.method);
+ if (req.method != "POST") {
+ sendError(res, "Bing only deals with POST requests, not '" + req.method + "'.");
+ return;
+ }
+
+ let body = getRequestBody(req);
+ log("body: " + body);
+
+ // First, we'll see if we're dealing with an XML body:
+ let contentType = req.hasHeader("Content-Type") ? req.getHeader("Content-Type") : null;
+ log("contentType: " + contentType);
+
+ if (contentType == "text/xml") {
+ try {
+ // For all these requests the client needs to supply the correct
+ // authentication headers.
+ checkAuth(req);
+
+ let xml = parseXml(body);
+ let method = xml.documentElement.localName;
+ log("invoking method: " + method);
+ // If the requested method is supported, delegate it to its handler.
+ if (methodHandlers[method])
+ methodHandlers[method](res, xml);
+ else
+ throw new HTTPError(501);
+ } catch (ex) {
+ sendError(res, ex, ex.code);
+ }
+ } else {
+ // Not XML, so it must be a query-string.
+ let params = parseQuery(body);
+
+ // Delegate an authentication request to the correct handler.
+ if ("grant_type" in params && params.grant_type == "client_credentials")
+ methodHandlers.authenticate(res, params);
+ else
+ sendError(res, 501);
+ }
+}
+
+const methodHandlers = {
+ authenticate: function(res, params) {
+ // Validate a few required parameters.
+ if (params.scope != "http://api.microsofttranslator.com") {
+ sendError(res, "Invalid scope.");
+ return;
+ }
+ if (!params.client_id) {
+ sendError(res, "Missing client_id param.");
+ return;
+ }
+ if (!params.client_secret) {
+ sendError(res, "Missing client_secret param.");
+ return;
+ }
+
+ let content = JSON.stringify({
+ access_token: "test",
+ expires_in: 600
+ });
+
+ res.setStatusLine("1.1", 200, "OK");
+ res.setHeader("Content-Length", String(content.length));
+ res.setHeader("Content-Type", "application/json");
+ res.write(content);
+ },
+
+ TranslateArrayRequest: function(res, xml, body) {
+ let from = xml.querySelector("From").firstChild.nodeValue;
+ let to = xml.querySelector("To").firstChild.nodeValue
+ log("translating from '" + from + "' to '" + to + "'");
+
+ res.setStatusLine("1.1", 200, "OK");
+ res.setHeader("Content-Type", "text/xml");
+
+ let hash = sha1(body).substr(0, 10);
+ log("SHA1 hash of content: " + hash);
+ let inputStream = getInputStream(
+ "browser/browser/components/translation/test/fixtures/result-" + hash + ".txt");
+ res.bodyOutputStream.writeFrom(inputStream, inputStream.available());
+ inputStream.close();
+ }
+};
diff --git a/browser/components/translation/test/browser.ini b/browser/components/translation/test/browser.ini
index 8760691c5b72..64052248db26 100644
--- a/browser/components/translation/test/browser.ini
+++ b/browser/components/translation/test/browser.ini
@@ -1,6 +1,10 @@
[DEFAULT]
+support-files =
+ bing.sjs
+ fixtures/bug1022725-fr.html
+ fixtures/result-da39a3ee5e.txt
+[browser_translation_bing.js]
[browser_translation_fhr.js]
-skip-if = true # Needs to wait until bug 1022725.
[browser_translation_infobar.js]
[browser_translation_exceptions.js]
diff --git a/browser/components/translation/test/browser_translation_bing.js b/browser/components/translation/test/browser_translation_bing.js
new file mode 100644
index 000000000000..7f9251df8d30
--- /dev/null
+++ b/browser/components/translation/test/browser_translation_bing.js
@@ -0,0 +1,56 @@
+/* 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/. */
+
+// Test the Bing Translator client against a mock Bing service, bing.sjs.
+
+"use strict";
+
+const kClientIdPref = "browser.translation.bing.clientIdOverride";
+const kClientSecretPref = "browser.translation.bing.apiKeyOverride";
+
+const {BingTranslator} = Cu.import("resource:///modules/translation/BingTranslator.jsm", {});
+const {TranslationDocument} = Cu.import("resource:///modules/translation/TranslationDocument.jsm", {});
+
+function test() {
+ waitForExplicitFinish();
+
+ Services.prefs.setCharPref(kClientIdPref, "testClient");
+ Services.prefs.setCharPref(kClientSecretPref, "testSecret");
+
+ // Deduce the Mochitest server address in use from a pref that was pre-processed.
+ let server = Services.prefs.getCharPref("browser.translation.bing.authURL")
+ .replace("http://", "");
+ server = server.substr(0, server.indexOf("/"));
+ let tab = gBrowser.addTab("http://" + server +
+ "/browser/browser/components/translation/test/fixtures/bug1022725-fr.html");
+ gBrowser.selectedTab = tab;
+
+ registerCleanupFunction(function () {
+ gBrowser.removeTab(tab);
+ Services.prefs.clearUserPref(kClientIdPref);
+ Services.prefs.clearUserPref(kClientSecretPref);
+ });
+
+ let browser = tab.linkedBrowser;
+ browser.addEventListener("load", function onload() {
+ if (browser.currentURI.spec == "about:blank")
+ return;
+
+ browser.removeEventListener("load", onload, true);
+ let client = new BingTranslator(
+ new TranslationDocument(browser.contentDocument), "fr", "en");
+
+ client.translate().then(
+ result => {
+ // XXXmikedeboer; here you would continue the test/ content inspection.
+ ok(result, "There should be a result.");
+ finish();
+ },
+ error => {
+ ok(false, "Unexpected Client Error: " + error);
+ finish();
+ }
+ );
+ }, true);
+}
diff --git a/browser/components/translation/test/browser_translation_fhr.js b/browser/components/translation/test/browser_translation_fhr.js
index 5ab4c1a6df20..cf14c8e6dd72 100644
--- a/browser/components/translation/test/browser_translation_fhr.js
+++ b/browser/components/translation/test/browser_translation_fhr.js
@@ -57,7 +57,8 @@ function retrieveTranslationCounts() {
return [0, 0];
}
- return [day.get("pageTranslatedCount"), day.get("charactersTranslatedCount")];
+ // .get() may return `undefined`, which we can't compute.
+ return [day.get("pageTranslatedCount") || 0, day.get("charactersTranslatedCount") || 0];
});
}
diff --git a/browser/components/translation/test/fixtures/bug1022725-fr.html b/browser/components/translation/test/fixtures/bug1022725-fr.html
new file mode 100644
index 000000000000..f30edf52eb20
--- /dev/null
+++ b/browser/components/translation/test/fixtures/bug1022725-fr.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ test
+
+
+
Coupe du monde de football de 2014
+
La Coupe du monde de football de 2014 est la 20e édition de la Coupe du monde de football, compétition organisée par la FIFA et qui réunit les trente-deux meilleures sélections nationales. Sa phase finale a lieu à l'été 2014 au Brésil. Avec le pays organisateur, toutes les équipes championnes du monde depuis 1930 (Uruguay, Italie, Allemagne, Angleterre, Argentine, France et Espagne) se sont qualifiées pour cette compétition. Elle est aussi la première compétition internationale de la Bosnie-Herzégovine.
+
+
diff --git a/browser/components/translation/test/fixtures/result-da39a3ee5e.txt b/browser/components/translation/test/fixtures/result-da39a3ee5e.txt
new file mode 100644
index 000000000000..d2d14c78852c
--- /dev/null
+++ b/browser/components/translation/test/fixtures/result-da39a3ee5e.txt
@@ -0,0 +1,22 @@
+
+
+ fr
+
+ 34
+
+ Football's 2014 World Cup
+
+ 25
+
+
+
+ fr
+
+ 508
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus diam sem, porttitor eget neque sit amet, ultricies posuere metus. Cras placerat rutrum risus, nec dignissim magna dictum vitae. Fusce eleifend fermentum lacinia. Nulla sagittis cursus nibh. Praesent adipiscing, elit at pulvinar dapibus, neque massa tincidunt sapien, eu consectetur lectus metus sit amet odio. Proin blandit consequat porttitor. Pellentesque vehicula justo sed luctus vestibulum. Donec metus.
+
+ 475
+
+
+
diff --git a/browser/components/translation/translation-infobar.xml b/browser/components/translation/translation-infobar.xml
index c63a4a262535..585bc3224401 100644
--- a/browser/components/translation/translation-infobar.xml
+++ b/browser/components/translation/translation-infobar.xml
@@ -152,11 +152,6 @@
diff --git a/browser/devtools/app-manager/simulators-store.js b/browser/devtools/app-manager/simulators-store.js
index d6626d0530c5..6fc757330b95 100644
--- a/browser/devtools/app-manager/simulators-store.js
+++ b/browser/devtools/app-manager/simulators-store.js
@@ -10,7 +10,11 @@ let store = new ObservableObject({versions:[]});
function feedStore() {
store.object.versions = Simulator.availableVersions().map(v => {
- return {version:v}
+ let simulator = Simulator.getByVersion(v);
+ return {
+ version: v,
+ label: simulator.appinfo.label
+ }
});
}
diff --git a/browser/devtools/webide/content/addons.js b/browser/devtools/webide/content/addons.js
new file mode 100644
index 000000000000..7da12b3cf2fd
--- /dev/null
+++ b/browser/devtools/webide/content/addons.js
@@ -0,0 +1,117 @@
+/* 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/. */
+
+const Cu = Components.utils;
+const {Services} = Cu.import("resource://gre/modules/Services.jsm");
+const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
+const {GetAvailableAddons} = require("devtools/webide/addons");
+const Strings = Services.strings.createBundle("chrome://webide/content/webide.properties");
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ document.querySelector("#aboutaddons").onclick = function() {
+ window.parent.UI.openInBrowser("about:addons");
+ }
+ document.querySelector("#close").onclick = CloseUI;
+ GetAvailableAddons().then(BuildUI, (e) => {
+ console.error(e);
+ window.alert(Strings.formatStringFromName("error_cantFetchAddonsJSON", [e], 1));
+ });
+}, true);
+
+function CloseUI() {
+ window.parent.UI.openProject();
+}
+
+function BuildUI(addons) {
+ BuildItem(addons.adb, true /* is adb */);
+ for (let addon of addons.simulators) {
+ BuildItem(addon, false /* is adb */);
+ }
+}
+
+function BuildItem(addon, isADB) {
+
+ function onAddonUpdate(event, arg) {
+ switch (event) {
+ case "update":
+ progress.removeAttribute("value");
+ li.setAttribute("status", addon.status);
+ status.textContent = Strings.GetStringFromName("addons_status_" + addon.status);
+ break;
+ case "failure":
+ console.error(arg);
+ window.alert(arg);
+ break;
+ case "progress":
+ if (arg == -1) {
+ progress.removeAttribute("value");
+ } else {
+ progress.value = arg;
+ }
+ break;
+ }
+ }
+
+ let events = ["update", "failure", "progress"];
+ for (let e of events) {
+ addon.on(e, onAddonUpdate);
+ }
+ window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ for (let e of events) {
+ addon.off(e, onAddonUpdate);
+ }
+ });
+
+ let li = document.createElement("li");
+ li.setAttribute("status", addon.status);
+
+ // Used in tests
+ if (isADB) {
+ li.setAttribute("addon", "adb");
+ } else {
+ li.setAttribute("addon", "simulator-" + addon.version);
+ }
+
+ let name = document.createElement("span");
+ name.className = "name";
+ if (isADB) {
+ name.textContent = Strings.GetStringFromName("addons_adb_label");
+ } else {
+ let stability = Strings.GetStringFromName("addons_" + addon.stability);
+ name.textContent = Strings.formatStringFromName("addons_simulator_label", [addon.version, stability], 2);
+ }
+
+ li.appendChild(name);
+
+ let status = document.createElement("span");
+ status.className = "status";
+ status.textContent = Strings.GetStringFromName("addons_status_" + addon.status);
+ li.appendChild(status);
+
+ let installButton = document.createElement("button");
+ installButton.className = "install-button";
+ installButton.onclick = () => addon.install();
+ installButton.textContent = Strings.GetStringFromName("addons_install_button");
+ li.appendChild(installButton);
+
+ let uninstallButton = document.createElement("button");
+ uninstallButton.className = "uninstall-button";
+ uninstallButton.onclick = () => addon.uninstall();
+ uninstallButton.textContent = Strings.GetStringFromName("addons_uninstall_button");
+ li.appendChild(uninstallButton);
+
+ let progress = document.createElement("progress");
+ li.appendChild(progress);
+
+ if (isADB) {
+ let warning = document.createElement("p");
+ warning.textContent = Strings.GetStringFromName("addons_adb_warning");
+ warning.className = "warning";
+ li.appendChild(warning);
+ }
+
+ document.querySelector("ul").appendChild(li);
+}
diff --git a/browser/devtools/webide/content/addons.xhtml b/browser/devtools/webide/content/addons.xhtml
new file mode 100644
index 000000000000..12ed00988c0d
--- /dev/null
+++ b/browser/devtools/webide/content/addons.xhtml
@@ -0,0 +1,30 @@
+
+
+
+
+
+ %webideDTD;
+]>
+
+
+
+
+
+
+
+
+
+