diff --git a/dom/apps/AppsServiceChild.jsm b/dom/apps/AppsServiceChild.jsm index 7b5d3bd366ff..86816225227e 100644 --- a/dom/apps/AppsServiceChild.jsm +++ b/dom/apps/AppsServiceChild.jsm @@ -354,6 +354,15 @@ this.DOMApplicationRegistry = { aCallback(res); }, + getAdditionalLanguages: function(aManifestURL) { + for (let id in this.webapps) { + if (this.webapps[id].manifestURL == aManifestURL) { + return this.webapps[id].additionalLanguages || {}; + } + } + return {}; + }, + /** * nsIAppsService API */ diff --git a/dom/apps/AppsUtils.jsm b/dom/apps/AppsUtils.jsm index 1c11dea0d04c..4dce2950d25d 100644 --- a/dom/apps/AppsUtils.jsm +++ b/dom/apps/AppsUtils.jsm @@ -22,6 +22,10 @@ XPCOMUtils.defineLazyModuleGetter(this, "WebappOSUtils", XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyServiceGetter(this, "appsService", + "@mozilla.org/AppsService;1", + "nsIAppsService"); + // Shared code for AppsServiceChild.jsm, TrustedHostedAppsUtils.jsm, // Webapps.jsm and Webapps.js @@ -485,6 +489,7 @@ this.AppsUtils = { * Checks if the app role is allowed: * Only certified apps can be themes. * Only privileged or certified apps can be addons. + * Langpacks need to be privileged. * @param aRole : the role assigned to this app. * @param aStatus : the APP_STATUS_* for this app. */ @@ -492,6 +497,13 @@ this.AppsUtils = { if (aRole == "theme" && aStatus !== Ci.nsIPrincipal.APP_STATUS_CERTIFIED) { return false; } + if (aRole == "langpack" && aStatus !== Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) { + let allow = false; + try { + allow = Services.prefs.getBoolPref("dom.apps.allow_unsigned_langpacks"); + } catch(e) {} + return allow; + } if (!this.allowUnsignedAddons && (aRole == "addon" && aStatus !== Ci.nsIPrincipal.APP_STATUS_CERTIFIED && @@ -732,7 +744,16 @@ this.AppsUtils = { // Returns the hash for a JS object. computeObjectHash: function(aObject) { return this.computeHash(JSON.stringify(aObject)); - } + }, + + getAppManifestURLFromWindow: function(aWindow) { + let appId = aWindow.document.nodePrincipal.appId; + if (appId === Ci.nsIScriptSecurityManager.NO_APP_ID) { + return null; + } + + return appsService.getManifestURLByLocalId(appId); + }, } /** diff --git a/dom/apps/Langpacks.jsm b/dom/apps/Langpacks.jsm new file mode 100644 index 000000000000..2b281abe1c4d --- /dev/null +++ b/dom/apps/Langpacks.jsm @@ -0,0 +1,315 @@ +/* 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 Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/AppsUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "ppmm", + "@mozilla.org/parentprocessmessagemanager;1", + "nsIMessageBroadcaster"); + +this.EXPORTED_SYMBOLS = ["Langpacks"]; + +let debug = Services.prefs.getBoolPref("dom.mozApps.debug") + ? (aMsg) => { + dump("-*-*- Langpacks: " + aMsg + "\n"); + } + : (aMsg) => {}; + +/** + * Langpack support + * + * Manifest format is: + * + * "languages-target" : { "app://*.gaiamobile.org/manifest.webapp": "2.2" }, + * "languages-provided": { + * "de": { + * "version": 201411051234, + * "name": "Deutsch", + * "apps": { + * "app://calendar.gaiamobile.org/manifest.webapp": "/de/calendar", + * "app://email.gaiamobile.org/manifest.webapp": "/de/email" + * } + * }, + * "role" : "langpack" + */ + +this.Langpacks = { + + _data: {}, + _broadcaster: null, + _appIdFromManifestURL: null, + + init: function() { + ppmm.addMessageListener("Webapps:GetLocalizationResource", this); + }, + + registerRegistryFunctions: function(aBroadcaster, aIdGetter) { + this._broadcaster = aBroadcaster; + this._appIdFromManifestURL = aIdGetter; + }, + + receiveMessage: function(aMessage) { + let data = aMessage.data; + let mm = aMessage.target; + switch (aMessage.name) { + case "Webapps:GetLocalizationResource": + this.getLocalizationResource(data, mm); + break; + default: + debug("Unexpected message: " + aMessage.name); + } + }, + + getAdditionalLanguages: function(aManifestURL) { + debug("getAdditionalLanguages " + aManifestURL); + let res = { langs: {} }; + let langs = res.langs; + if (this._data[aManifestURL]) { + res.appId = this._data[aManifestURL].appId; + for (let lang in this._data[aManifestURL].langs) { + if (!langs[lang]) { + langs[lang] = []; + } + let current = this._data[aManifestURL].langs[lang]; + langs[lang].push({ + version: current.version, + name: current.name, + target: current.target + }); + } + } + debug("Languages found: " + uneval(res)); + return res; + }, + + sendAppUpdate: function(aManifestURL) { + debug("sendAppUpdate " + aManifestURL); + if (!this._broadcaster) { + debug("No broadcaster!"); + return; + } + + let res = this.getAdditionalLanguages(aManifestURL); + let message = { + id: res.appId, + app: { + additionalLanguages: res.langs + } + } + this._broadcaster("Webapps:UpdateState", message); + }, + + getLocalizationResource: function(aData, aMm) { + debug("getLocalizationResource " + uneval(aData)); + + function sendError(aMsg, aCode) { + debug(aMsg); + aMm.sendAsyncMessage("Webapps:GetLocalizationResource:Return", + { requestID: aData.requestID, oid: aData.oid, error: aCode }); + } + + // No langpack available for this app. + if (!this._data[aData.manifestURL]) { + return sendError("No langpack for this app.", "NoLangpack"); + } + + // We have langpack(s) for this app, but not for this language. + if (!this._data[aData.manifestURL].langs[aData.lang]) { + return sendError("No language " + aData.lang + " for this app.", + "UnavailableLanguage"); + } + + // Check that we have the right version. + let item = this._data[aData.manifestURL].langs[aData.lang]; + if (item.target != aData.version) { + return sendError("No version " + aData.version + " for this app.", + "UnavailableVersion"); + } + + // The path can't be an absolute uri. + if (isAbsoluteURI(aData.path)) { + return sendError("url can't be absolute.", "BadUrl"); + } + + let href = item.url + aData.path; + debug("Will load " + href); + + let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + xhr.mozBackgroundRequest = true; + xhr.open("GET", href); + + // Default to text response type, but the webidl binding takes care of + // validating the dataType value. + xhr.responseType = "text"; + if (aData.dataType === "json") { + xhr.responseType = "json"; + } else if (aData.dataType === "binary") { + xhr.responseType = "blob"; + } + + xhr.addEventListener("load", function() { + debug("Success loading " + href); + if (xhr.status >= 200 && xhr.status < 400) { + aMm.sendAsyncMessage("Webapps:GetLocalizationResource:Return", + { requestID: aData.requestID, oid: aData.oid, data: xhr.response }); + } else { + sendError("Error loading " + href, "UnavailableResource"); + } + }); + xhr.addEventListener("error", function() { + sendError("Error loading " + href, "UnavailableResource"); + }); + xhr.send(null); + }, + + // Validates the langpack part of a manifest. + checkManifest: function(aManifest) { + if (!("languages-target" in aManifest)) { + debug("Error: no 'languages-target' property.") + return false; + } + + if (!("languages-provided" in aManifest)) { + debug("Error: no 'languages-provided' property.") + return false; + } + + for (let lang in aManifest["languages-provided"]) { + let item = aManifest["languages-provided"][lang]; + + if (!item.version) { + debug("Error: missing 'version' in languages-provided." + lang); + return false; + } + + if (typeof item.version !== "number") { + debug("Error: languages-provided." + lang + + ".version must be a number but is a " + (typeof item.version)); + return false; + } + + if (!item.apps) { + debug("Error: missing 'apps' in languages-provided." + lang); + return false; + } + + for (let app in item.apps) { + // Keys should be manifest urls, ie. absolute urls. + if (!isAbsoluteURI(app)) { + debug("Error: languages-provided." + lang + "." + app + + " must be an absolute manifest url."); + return false; + } + + if (typeof item.apps[app] !== "string") { + debug("Error: languages-provided." + lang + ".apps." + app + + " value must be a string but is " + (typeof item.apps[app]) + + " : " + item.apps[app]); + return false; + } + } + } + return true; + }, + + // Check if this app is a langpack and update registration if needed. + register: function(aApp, aManifest) { + debug("register app " + aApp.manifestURL + " role=" + aApp.role); + + if (aApp.role !== "langpack") { + debug("Not a langpack."); + // Not a langpack, but that's fine. + return; + } + + if (!this.checkManifest(aManifest)) { + debug("Invalid langpack manifest."); + return; + } + + let platformVersion = aManifest["languages-target"] + ["app://*.gaiamobile.org/manifest.webapp"]; + let origin = Services.io.newURI(aApp.origin, null, null); + + for (let lang in aManifest["languages-provided"]) { + let item = aManifest["languages-provided"][lang]; + let version = item.version; // The langpack version, not the platform. + let name = item.name || lang; // If no name specified, default to lang. + for (let app in item.apps) { + let sendEvent = false; + if (!this._data[app] || + !this._data[app].langs[lang] || + this._data[app].langs[lang].version > version) { + if (!this._data[app]) { + this._data[app] = { + appId: this._appIdFromManifestURL(app), + langs: {} + }; + } + this._data[app].langs[lang] = { + version: version, + target: platformVersion, + name: name, + url: origin.resolve(item.apps[app]), + from: aApp.manifestURL + } + sendEvent = true; + debug("Registered " + app + " -> " + uneval(this._data[app].langs[lang])); + } + + // Fire additionallanguageschange event. + // This will only be dispatched to documents using the langpack api. + if (sendEvent) { + this.sendAppUpdate(app); + ppmm.broadcastAsyncMessage( + "Webapps:AdditionalLanguageChange", + { manifestURL: app, + languages: this.getAdditionalLanguages(app).langs }); + } + } + } + }, + + // Check if this app is a langpack and update registration by removing all + // the entries from this app. + unregister: function(aApp, aManifest) { + debug("unregister app " + aApp.manifestURL + " role=" + aApp.role); + + if (aApp.role !== "langpack") { + debug("Not a langpack."); + // Not a langpack, but that's fine. + return; + } + + for (let app in this._data) { + let sendEvent = false; + for (let lang in this._data[app].langs) { + if (this._data[app].langs[lang].from == aApp.manifestURL) { + sendEvent = true; + delete this._data[app].langs[lang]; + } + } + // Fire additionallanguageschange event. + // This will only be dispatched to documents using the langpack api. + if (sendEvent) { + this.sendAppUpdate(app); + ppmm.broadcastAsyncMessage( + "Webapps:AdditionalLanguageChange", + { manifestURL: app, + languages: this.getAdditionalLanguages(app).langs }); + } + } + } +} + +Langpacks.init(); \ No newline at end of file diff --git a/dom/apps/Webapps.js b/dom/apps/Webapps.js index 0d3c6c361704..bf268bbc2223 100644 --- a/dom/apps/Webapps.js +++ b/dom/apps/Webapps.js @@ -44,11 +44,18 @@ WebappsRegistry.prototype = { receiveMessage: function(aMessage) { let msg = aMessage.json; - if (msg.oid != this._id) - return - let req = this.getRequest(msg.requestID); - if (!req) - return; + let req; + if (msg.oid === this._id) { + if (aMessage.name == "Webapps:GetLocalizationResource:Return") { + req = this.takePromiseResolver(msg.requestID); + } else { + req = this.getRequest(msg.requestID); + } + if (!req) { + return; + } + } + let app = msg.app; switch (aMessage.name) { case "Webapps:Install:Return:OK": @@ -78,6 +85,26 @@ WebappsRegistry.prototype = { this.removeMessageListeners(aMessage.name); Services.DOMRequest.fireSuccess(req, convertAppsArray(msg.apps, this._window)); break; + case "Webapps:AdditionalLanguageChange": + // Check if the current page is from the app receiving the event. + let manifestURL = AppsUtils.getAppManifestURLFromWindow(this._window); + if (manifestURL && manifestURL == msg.manifestURL) { + // Let's dispatch an "additionallanguageschange" event on the document. + let doc = this._window.document; + let event = doc.createEvent("CustomEvent"); + event.initCustomEvent("additionallanguageschange", true, true, + Cu.cloneInto(msg.languages, this._window)); + doc.dispatchEvent(event); + } + break; + case "Webapps:GetLocalizationResource:Return": + this.removeMessageListeners(["Webapps:GetLocalizationResource:Return"]); + if (msg.error) { + req.reject(new this._window.DOMError(msg.error)); + } else { + req.resolve(Cu.cloneInto(msg.data, this._window)); + } + break; } this.removeRequest(msg.requestID); }, @@ -231,7 +258,8 @@ WebappsRegistry.prototype = { uninit: function() { this._mgmt = null; cpmm.sendAsyncMessage("Webapps:UnregisterForMessages", - ["Webapps:Install:Return:OK"]); + ["Webapps:Install:Return:OK", + "Webapps:AdditionalLanguageChange"]); }, installPackage: function(aURL, aParams) { @@ -248,19 +276,69 @@ WebappsRegistry.prototype = { return request; }, + _getCurrentAppManifestURL: function() { + let appId = this._window.document.nodePrincipal.appId; + if (appId === Ci.nsIScriptSecurityManager.NO_APP_ID) { + return null; + } + + return appsService.getManifestURLByLocalId(appId); + }, + + getAdditionalLanguages: function() { + let manifestURL = AppsUtils.getAppManifestURLFromWindow(this._window); + + return new this._window.Promise((aResolve, aReject) => { + if (!manifestURL) { + aReject("NotInApp"); + } else { + let langs = DOMApplicationRegistry.getAdditionalLanguages(manifestURL); + aResolve(Cu.cloneInto(langs, this._window)); + } + }); + }, + + getLocalizationResource: function(aLanguage, aVersion, aPath, aType) { + let manifestURL = AppsUtils.getAppManifestURLFromWindow(this._window); + + if (!manifestURL) { + return new Promise((aResolve, aReject) => { + aReject("NotInApp"); + }); + } + + this.addMessageListeners(["Webapps:GetLocalizationResource:Return"]); + return this.createPromise((aResolve, aReject) => { + cpmm.sendAsyncMessage("Webapps:GetLocalizationResource", { + manifestURL: manifestURL, + lang: aLanguage, + version: aVersion, + path: aPath, + dataType: aType, + oid: this._id, + requestID: this.getPromiseResolverId({ + resolve: aResolve, + reject: aReject + }) + }); + }); + }, + // nsIDOMGlobalPropertyInitializer implementation init: function(aWindow) { const prefs = new Preferences(); this._window = aWindow; - this.initDOMRequestHelper(aWindow, "Webapps:Install:Return:OK"); + this.initDOMRequestHelper(aWindow, ["Webapps:Install:Return:OK", + "Webapps:AdditionalLanguageChange"]); let util = this._window.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); this._id = util.outerWindowID; cpmm.sendAsyncMessage("Webapps:RegisterForMessages", - { messages: ["Webapps:Install:Return:OK"]}); + { messages: ["Webapps:Install:Return:OK", + "Webapps:AdditionalLanguageChange"]}); let principal = aWindow.document.nodePrincipal; let appId = principal.appId; diff --git a/dom/apps/Webapps.jsm b/dom/apps/Webapps.jsm index 345ce8b50a2e..82f39505b3df 100755 --- a/dom/apps/Webapps.jsm +++ b/dom/apps/Webapps.jsm @@ -81,6 +81,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", XPCOMUtils.defineLazyModuleGetter(this, "ScriptPreloader", "resource://gre/modules/ScriptPreloader.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Langpacks", + "resource://gre/modules/Langpacks.jsm"); + XPCOMUtils.defineLazyModuleGetter(this, "TrustedHostedAppsUtils", "resource://gre/modules/TrustedHostedAppsUtils.jsm"); @@ -243,6 +246,9 @@ this.DOMApplicationRegistry = { ["webapps", "webapps.json"], true).path; this.loadAndUpdateApps(); + + Langpacks.registerRegistryFunctions(this.broadcastMessage.bind(this), + this._appIdForManifestURL.bind(this)); }, // loads the current registry, that could be empty on first run. @@ -424,6 +430,7 @@ this.DOMApplicationRegistry = { } app.kind = this.appKind(app, aResult.manifest); UserCustomizations.register(aResult.manifest, app); + Langpacks.register(app, aResult.manifest); }); // Nothing else to do but notifying we're ready. @@ -1149,6 +1156,7 @@ this.DOMApplicationRegistry = { this._registerInterAppConnections(manifest, app); appsToRegister.push({ manifest: manifest, app: app }); UserCustomizations.register(manifest, app); + Langpacks.register(app, manifest); }); this._safeToClone.resolve(); this._registerActivitiesForApps(appsToRegister, aRunUpdate); @@ -1520,6 +1528,8 @@ this.DOMApplicationRegistry = { this.safeToClone.then( () => { for (let id in this.webapps) { tmp.push({ id: id }); + this.webapps[id].additionalLanguages = + Langpacks.getAdditionalLanguages(this.webapps[id].manifestURL).langs; } this._readManifests(tmp).then( function(manifests) { @@ -1964,6 +1974,10 @@ this.DOMApplicationRegistry = { // Update the asm.js scripts we need to compile. yield ScriptPreloader.preload(app, newManifest); + + // Update langpack information. + Langpacks.register(app, newManifest); + yield this._saveApps(); // Update the handlers and permissions for this app. this.updateAppHandlers(oldManifest, newManifest, app); @@ -2081,11 +2095,13 @@ this.DOMApplicationRegistry = { this.notifyAppsRegistryReady(); } - // Update user customizations. + // Update user customizations and langpacks. if (aOldManifest) { UserCustomizations.unregister(aOldManifest, aApp); + Langpacks.unregister(aApp, aOldManifest); } UserCustomizations.register(aNewManifest, aApp); + Langpacks.register(aApp, aNewManifest); }, checkForUpdate: function(aData, aMm) { @@ -3185,6 +3201,9 @@ this.DOMApplicationRegistry = { // Check if we have asm.js code to preload for this application. yield ScriptPreloader.preload(aNewApp, aManifest); + // Update langpack information. + yield Langpacks.register(aNewApp, aManifest); + this.broadcastMessage("Webapps:FireEvent", { eventType: ["downloadsuccess", "downloadapplied"], manifestURL: aNewApp.manifestURL @@ -4094,6 +4113,7 @@ this.DOMApplicationRegistry = { this._unregisterActivities(aApp.manifest, aApp); } UserCustomizations.unregister(aApp.manifest, aApp); + Langpacks.unregister(aApp, aApp.manifest); let dir = this._getAppDir(id); try { diff --git a/dom/apps/moz.build b/dom/apps/moz.build index 925e7b5a7304..46966146caf7 100644 --- a/dom/apps/moz.build +++ b/dom/apps/moz.build @@ -34,6 +34,7 @@ EXTRA_JS_MODULES += [ 'AppsServiceChild.jsm', 'FreeSpaceWatcher.jsm', 'InterAppCommService.jsm', + 'Langpacks.jsm', 'OfflineCacheInstaller.jsm', 'PermissionsInstaller.jsm', 'PermissionsTable.jsm', diff --git a/dom/apps/tests/langpack/event.html b/dom/apps/tests/langpack/event.html new file mode 100644 index 000000000000..ca9170d083de --- /dev/null +++ b/dom/apps/tests/langpack/event.html @@ -0,0 +1,28 @@ + + +
+++ + + diff --git a/dom/tests/mochitest/webapps/test_list_api.xul b/dom/tests/mochitest/webapps/test_list_api.xul index f5a4c9ee86af..0adc50986d16 100644 --- a/dom/tests/mochitest/webapps/test_list_api.xul +++ b/dom/tests/mochitest/webapps/test_list_api.xul @@ -20,7 +20,9 @@ var props = { checkInstalled: "function", + getAdditionalLanguages: "function", getInstalled: "function", + getLocalizationResource: "function", getSelf: "function", install: "function", installPackage: "function", diff --git a/dom/webidl/Apps.webidl b/dom/webidl/Apps.webidl index 212ac8a415ec..37fd9e9c6cfb 100644 --- a/dom/webidl/Apps.webidl +++ b/dom/webidl/Apps.webidl @@ -9,6 +9,18 @@ dictionary InstallParameters { sequence