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; +]> + + + + + + + + + +
+ &addons_aboutaddons; + &deck_close; +
+ +

&addons_title;

+ + + + + diff --git a/browser/devtools/webide/content/jar.mn b/browser/devtools/webide/content/jar.mn index dbc606018422..1e17d4f3df9d 100644 --- a/browser/devtools/webide/content/jar.mn +++ b/browser/devtools/webide/content/jar.mn @@ -11,6 +11,12 @@ webide.jar: content/details.xhtml (details.xhtml) content/details.js (details.js) content/cli.js (cli.js) + content/addons.js (addons.js) + content/addons.xhtml (addons.xhtml) + content/permissionstable.js (permissionstable.js) + content/permissionstable.xhtml (permissionstable.xhtml) + content/runtimedetails.js (runtimedetails.js) + content/runtimedetails.xhtml (runtimedetails.xhtml) # Temporarily include locales in content, until we're ready # to localize webide diff --git a/browser/devtools/webide/content/newapp.js b/browser/devtools/webide/content/newapp.js index e6451b4ee985..83f5ea18a0f6 100644 --- a/browser/devtools/webide/content/newapp.js +++ b/browser/devtools/webide/content/newapp.js @@ -17,6 +17,7 @@ const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm"); const {AppProjects} = require("devtools/app-manager/app-projects"); const APP_CREATOR_LIST = "devtools.webide.templatesURL"; const {AppManager} = require("devtools/webide/app-manager"); +const {GetTemplatesJSON} = require("devtools/webide/remote-resources"); let gTemplateList = null; @@ -33,20 +34,12 @@ window.addEventListener("load", function onLoad() { }, true); function getJSON() { - let xhr = new XMLHttpRequest(); - xhr.overrideMimeType('text/plain'); - xhr.onload = function() { - let list; - try { - list = JSON.parse(this.responseText); - if (!Array.isArray(list)) { - throw new Error("JSON response not an array"); - } - if (list.length == 0) { - throw new Error("JSON response is an empty array"); - } - } catch(e) { - return failAndBail("Invalid response from server"); + GetTemplatesJSON().then(list => { + if (!Array.isArray(list)) { + throw new Error("JSON response not an array"); + } + if (list.length == 0) { + throw new Error("JSON response is an empty array"); } gTemplateList = list; let templatelistNode = document.querySelector("#templatelist"); @@ -76,13 +69,9 @@ function getJSON() { document.querySelector("#project-name").value = testOptions.name; doOK(); } - }; - xhr.onerror = function() { - failAndBail("Can't download app templates"); - }; - let url = Services.prefs.getCharPref(APP_CREATOR_LIST); - xhr.open("get", url); - xhr.send(); + }, (e) => { + failAndBail("Can't download app templates: " + e); + }); } function failAndBail(msg) { diff --git a/browser/devtools/webide/content/permissionstable.js b/browser/devtools/webide/content/permissionstable.js new file mode 100644 index 000000000000..6dc356bfeeb6 --- /dev/null +++ b/browser/devtools/webide/content/permissionstable.js @@ -0,0 +1,75 @@ +/* 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 {AppManager} = require("devtools/webide/app-manager"); +const {Connection} = require("devtools/client/connection-manager"); + +window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad); + document.querySelector("#close").onclick = CloseUI; + AppManager.on("app-manager-update", OnAppManagerUpdate); + BuildUI(); +}, true); + +window.addEventListener("unload", function onUnload() { + window.removeEventListener("unload", onUnload); + AppManager.off("app-manager-update", OnAppManagerUpdate); +}); + +function CloseUI() { + window.parent.UI.openProject(); +} + +function OnAppManagerUpdate(event, what) { + if (what == "connection" || what == "list-tabs-response") { + BuildUI(); + } +} + +let getRawPermissionsTablePromise; // Used by tests +function BuildUI() { + let table = document.querySelector("table"); + let lines = table.querySelectorAll(".line"); + for (let line of lines) { + line.remove(); + } + + if (AppManager.connection && + AppManager.connection.status == Connection.Status.CONNECTED && + AppManager.deviceFront) { + getRawPermissionsTablePromise = AppManager.deviceFront.getRawPermissionsTable(); + getRawPermissionsTablePromise.then(json => { + let permissionsTable = json.rawPermissionsTable; + for (let name in permissionsTable) { + let tr = document.createElement("tr"); + tr.className = "line"; + let td = document.createElement("td"); + td.textContent = name; + tr.appendChild(td); + for (let type of ["app","privileged","certified"]) { + let td = document.createElement("td"); + if (permissionsTable[name][type] == json.ALLOW_ACTION) { + td.textContent = "✓"; + td.className = "permallow"; + } + if (permissionsTable[name][type] == json.PROMPT_ACTION) { + td.textContent = "!"; + td.className = "permprompt"; + } + if (permissionsTable[name][type] == json.DENY_ACTION) { + td.textContent = "✕"; + td.className = "permdeny" + } + tr.appendChild(td); + } + table.appendChild(tr); + } + }); + } else { + CloseUI(); + } +} diff --git a/browser/devtools/webide/content/permissionstable.xhtml b/browser/devtools/webide/content/permissionstable.xhtml new file mode 100644 index 000000000000..17dffd1b9e36 --- /dev/null +++ b/browser/devtools/webide/content/permissionstable.xhtml @@ -0,0 +1,35 @@ + + + + + + %webideDTD; +]> + + + + + + + + + +
+ &deck_close; +
+ +

&permissionstable_title;

+ + + + + + + + +
&permissionstable_name_header;type:webtype:privilegedtype:certified
+ + diff --git a/browser/devtools/webide/content/runtimedetails.js b/browser/devtools/webide/content/runtimedetails.js new file mode 100644 index 000000000000..8de2196dada2 --- /dev/null +++ b/browser/devtools/webide/content/runtimedetails.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/. */ + +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 {AppManager} = require("devtools/webide/app-manager"); +const {Connection} = require("devtools/client/connection-manager"); + +window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad); + document.querySelector("#close").onclick = CloseUI; + AppManager.on("app-manager-update", OnAppManagerUpdate); + BuildUI(); +}, true); + +window.addEventListener("unload", function onUnload() { + window.removeEventListener("unload", onUnload); + AppManager.off("app-manager-update", OnAppManagerUpdate); +}); + +function CloseUI() { + window.parent.UI.openProject(); +} + +function OnAppManagerUpdate(event, what) { + if (what == "connection" || what == "list-tabs-response") { + BuildUI(); + } +} + +let getDescriptionPromise; // Used by tests +function BuildUI() { + let table = document.querySelector("table"); + table.innerHTML = ""; + if (AppManager.connection && + AppManager.connection.status == Connection.Status.CONNECTED && + AppManager.deviceFront) { + getDescriptionPromise = AppManager.deviceFront.getDescription(); + getDescriptionPromise.then(json => { + for (let name in json) { + let tr = document.createElement("tr"); + let td = document.createElement("td"); + td.textContent = name; + tr.appendChild(td); + td = document.createElement("td"); + td.textContent = json[name]; + tr.appendChild(td); + table.appendChild(tr); + } + }); + } else { + CloseUI(); + } +} diff --git a/browser/devtools/webide/content/runtimedetails.xhtml b/browser/devtools/webide/content/runtimedetails.xhtml new file mode 100644 index 000000000000..6d1f5d6dec1f --- /dev/null +++ b/browser/devtools/webide/content/runtimedetails.xhtml @@ -0,0 +1,28 @@ + + + + + + %webideDTD; +]> + + + + + + + + + +
+ &deck_close; +
+ +

&runtimedetails_title;

+ +
+ + diff --git a/browser/devtools/webide/content/webide.js b/browser/devtools/webide/content/webide.js index 1e8d6ec12814..631247872805 100644 --- a/browser/devtools/webide/content/webide.js +++ b/browser/devtools/webide/content/webide.js @@ -18,12 +18,19 @@ const {Connection} = require("devtools/client/connection-manager"); const {AppManager} = require("devtools/webide/app-manager"); const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); const ProjectEditor = require("projecteditor/projecteditor"); +const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm"); +const {GetAvailableAddons} = require("devtools/webide/addons"); +const {GetTemplatesJSON, GetAddonsJSON} = require("devtools/webide/remote-resources"); const Strings = Services.strings.createBundle("chrome://webide/content/webide.properties"); const HTML = "http://www.w3.org/1999/xhtml"; const HELP_URL = "https://developer.mozilla.org/Firefox_OS/Using_the_App_Manager#Troubleshooting"; +// download some JSON early. +GetTemplatesJSON(true); +GetAddonsJSON(true); + // See bug 989619 console.log = console.log.bind(console); console.warn = console.warn.bind(console); @@ -56,18 +63,32 @@ let UI = { window.addEventListener("focus", this.onfocus, true); AppProjects.load().then(() => { - let lastProjectLocation = Services.prefs.getCharPref("devtools.webide.lastprojectlocation"); - if (lastProjectLocation) { - let lastProject = AppProjects.get(lastProjectLocation); - if (lastProject) { - AppManager.selectedProject = lastProject; - } else { - AppManager.selectedProject = null; - } + this.openLastProject(); + }); + + // Auto install the ADB Addon Helper. Only once. + // If the user decides to uninstall the addon, we won't install it again. + let autoInstallADBHelper = Services.prefs.getBoolPref("devtools.webide.autoinstallADBHelper"); + if (autoInstallADBHelper && !Devices.helperAddonInstalled) { + GetAvailableAddons().then(addons => { + addons.adb.install(); + }, console.error); + } + Services.prefs.setBoolPref("devtools.webide.autoinstallADBHelper", false); + }, + + openLastProject: function() { + let lastProjectLocation = Services.prefs.getCharPref("devtools.webide.lastprojectlocation"); + if (lastProjectLocation) { + let lastProject = AppProjects.get(lastProjectLocation); + if (lastProject) { + AppManager.selectedProject = lastProject; } else { AppManager.selectedProject = null; } - }); + } else { + AppManager.selectedProject = null; + } }, uninit: function() { @@ -203,7 +224,7 @@ let UI = { } }]; - let nbox = document.querySelector("#body"); + let nbox = document.querySelector("#notificationbox"); nbox.removeAllNotifications(true); nbox.appendNotification(text, "webide:errornotification", null, nbox.PRIORITY_WARNING_LOW, buttons); @@ -216,6 +237,28 @@ let UI = { let simulatorListNode = document.querySelector("#runtime-panel-simulators"); let customListNode = document.querySelector("#runtime-panel-custom"); + let noHelperNode = document.querySelector("#runtime-panel-noadbhelper"); + let noUSBNode = document.querySelector("#runtime-panel-nousbdevice"); + let noSimulatorNode = document.querySelector("#runtime-panel-nosimulator"); + + if (Devices.helperAddonInstalled) { + noHelperNode.setAttribute("hidden", "true"); + } else { + noHelperNode.removeAttribute("hidden"); + } + + if (AppManager.runtimeList.usb.length == 0 && Devices.helperAddonInstalled) { + noUSBNode.removeAttribute("hidden"); + } else { + noUSBNode.setAttribute("hidden", "true"); + } + + if (AppManager.runtimeList.simulator.length > 0) { + noSimulatorNode.setAttribute("hidden", "true"); + } else { + noSimulatorNode.removeAttribute("hidden"); + } + for (let [type, parent] of [ ["usb", USBListNode], ["simulator", simulatorListNode], @@ -283,7 +326,7 @@ let UI = { return this.projecteditor.loaded; } - let projecteditorIframe = document.querySelector("#projecteditor"); + let projecteditorIframe = document.querySelector("#deck-panel-projecteditor"); this.projecteditor = ProjectEditor.ProjectEditor(projecteditorIframe); this.projecteditor.on("onEditorSave", (editor, resource) => { AppManager.validateProject(AppManager.selectedProject); @@ -306,7 +349,7 @@ let UI = { iconUrl: project.icon, projectOverviewURL: "chrome://webide/content/details.xhtml", validationStatus: status - }); + }).then(null, console.error); }, console.error); }, @@ -315,17 +358,12 @@ let UI = { }, openProject: function() { - let detailsIframe = document.querySelector("#details"); - let projecteditorIframe = document.querySelector("#projecteditor"); - let project = AppManager.selectedProject; // Nothing to show if (!project) { - detailsIframe.setAttribute("hidden", "true"); - projecteditorIframe.setAttribute("hidden", "true"); - document.commandDispatcher.focusedElement = document.documentElement; + this.resetDeck(); return; } @@ -342,16 +380,13 @@ let UI = { if (project.type != "packaged" || !this.isProjectEditorEnabled() || forceDetailsOnly) { - detailsIframe.removeAttribute("hidden"); - projecteditorIframe.setAttribute("hidden", "true"); - document.commandDispatcher.focusedElement = document.documentElement; + this.selectDeckPanel("details"); return; } // Show ProjectEditor - detailsIframe.setAttribute("hidden", "true"); - projecteditorIframe.removeAttribute("hidden"); + this.selectDeckPanel("projecteditor"); this.getProjectEditor().then(() => { this.updateProjectEditorHeader(); @@ -362,6 +397,26 @@ let UI = { } }, + /********** DECK **********/ + + resetFocus: function() { + document.commandDispatcher.focusedElement = document.documentElement; + }, + + selectDeckPanel: function(id) { + this.hidePanels(); + this.resetFocus(); + let deck = document.querySelector("#deck"); + let panel = deck.querySelector("#deck-panel-" + id); + deck.selectedPanel = panel; + }, + + resetDeck: function() { + this.resetFocus(); + let deck = document.querySelector("#deck"); + deck.selectedPanel = null; + }, + /********** COMMANDS **********/ updateCommands: function() { @@ -506,9 +561,7 @@ let UI = { }, closeToolboxUI: function() { - let body = document.querySelector("#body"); - body.removeAttribute("hidden"); - + this.resetFocus(); Services.prefs.setIntPref("devtools.toolbox.footer.height", this.toolboxIframe.height); // We have to destroy the iframe, otherwise, the keybindings of webide don't work @@ -727,81 +780,11 @@ let Cmds = { }, showPermissionsTable: function() { - return UI.busyUntil(AppManager.deviceFront.getRawPermissionsTable().then(json => { - let styleContent = ""; - styleContent += "body {background:white; font-family: monospace}"; - styleContent += "table {border-collapse: collapse}"; - styleContent += "th, td {padding: 5px; border: 1px solid #EEE}"; - styleContent += "th {min-width: 130px}"; - styleContent += "td {text-align: center}"; - styleContent += "th:first-of-type, td:first-of-type {text-align:left}"; - styleContent += ".permallow {color:rgb(152, 207, 57)}"; - styleContent += ".permprompt {color:rgb(0,158,237)}"; - styleContent += ".permdeny {color:rgb(204,73,8)}"; - let style = document.createElementNS(HTML, "style"); - style.textContent = styleContent; - let table = document.createElementNS(HTML, "table"); - table.innerHTML = "Nametype:webtype:privilegedtype:certified"; - let permissionsTable = json.rawPermissionsTable; - for (let name in permissionsTable) { - let tr = document.createElementNS(HTML, "tr"); - let td = document.createElementNS(HTML, "td"); - td.textContent = name; - tr.appendChild(td); - for (let type of ["app","privileged","certified"]) { - let td = document.createElementNS(HTML, "td"); - if (permissionsTable[name][type] == json.ALLOW_ACTION) { - td.textContent = "✓"; - td.className = "permallow"; - } - if (permissionsTable[name][type] == json.PROMPT_ACTION) { - td.textContent = "!"; - td.className = "permprompt"; - } - if (permissionsTable[name][type] == json.DENY_ACTION) { - td.textContent = "✕"; - td.className = "permdeny" - } - tr.appendChild(td); - } - table.appendChild(tr); - } - let body = document.createElementNS(HTML, "body"); - body.appendChild(style); - body.appendChild(table); - let url = "data:text/html;charset=utf-8,"; - url += encodeURIComponent(body.outerHTML); - UI.openInBrowser(url); - }), "showing permission table"); + UI.selectDeckPanel("permissionstable"); }, showRuntimeDetails: function() { - return UI.busyUntil(AppManager.deviceFront.getDescription().then(json => { - let styleContent = ""; - styleContent += "body {background:white; font-family: monospace}"; - styleContent += "table {border-collapse: collapse}"; - styleContent += "th, td {padding: 5px; border: 1px solid #EEE}"; - let style = document.createElementNS(HTML, "style"); - style.textContent = styleContent; - let table = document.createElementNS(HTML, "table"); - for (let name in json) { - let tr = document.createElementNS(HTML, "tr"); - let td = document.createElementNS(HTML, "td"); - td.textContent = name; - tr.appendChild(td); - td = document.createElementNS(HTML, "td"); - td.textContent = json[name]; - tr.appendChild(td); - table.appendChild(tr); - } - let body = document.createElementNS(HTML, "body"); - body.appendChild(style); - body.appendChild(table); - let url = "data:text/html;charset=utf-8,"; - url += encodeURIComponent(body.outerHTML); - UI.openInBrowser(url); - }), "showing runtime details"); - + UI.selectDeckPanel("runtimedetails"); }, play: function() { @@ -846,4 +829,8 @@ let Cmds = { showTroubleShooting: function() { UI.openInBrowser(HELP_URL); }, + + showAddons: function() { + UI.selectDeckPanel("addons"); + }, } diff --git a/browser/devtools/webide/content/webide.xul b/browser/devtools/webide/content/webide.xul index 4d3da7984236..3a77c8eb22eb 100644 --- a/browser/devtools/webide/content/webide.xul +++ b/browser/devtools/webide/content/webide.xul @@ -41,6 +41,8 @@ + + @@ -76,6 +78,7 @@ + @@ -136,8 +139,11 @@ + + + @@ -151,9 +157,14 @@ - -