--- a/dom/apps/AppsServiceChild.jsm
+++ b/dom/apps/AppsServiceChild.jsm
@@ -349,16 +349,25 @@ this.DOMApplicationRegistry = {
let res = [];
for (let id in this.webapps) {
res.push(this.webapps[id]);
}
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
*/
getAppByManifestURL: function getAppByManifestURL(aManifestURL) {
debug("getAppByManifestURL " + aManifestURL);
return AppsUtils.getAppByManifestURL(this.webapps, aManifestURL);
},
--- a/dom/apps/AppsUtils.jsm
+++ b/dom/apps/AppsUtils.jsm
@@ -17,16 +17,20 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WebappOSUtils",
"resource://gre/modules/WebappOSUtils.jsm");
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
this.EXPORTED_SYMBOLS =
["AppsUtils", "ManifestHelper", "isAbsoluteURI", "mozIApplication"];
function debug(s) {
//dump("-*- AppsUtils.jsm: " + s + "\n");
@@ -480,23 +484,31 @@ this.AppsUtils = {
},
allowUnsignedAddons: false, // for testing purposes.
/**
* 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.
*/
checkAppRole: function(aRole, aStatus) {
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 &&
aStatus !== Ci.nsIPrincipal.APP_STATUS_PRIVILEGED)) {
return false;
}
return true;
},
@@ -727,17 +739,26 @@ this.AppsUtils = {
// Convert the binary hash data to a hex string.
return [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
},
// 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);
+ },
}
/**
* Helper object to access manifest information with locale support
*/
this.ManifestHelper = function(aManifest, aOrigin, aManifestURL) {
// If the app is packaged, we resolve uris against the origin.
// If it's not, against the manifest url.
new file mode 100644
--- /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
--- a/dom/apps/Webapps.js
+++ b/dom/apps/Webapps.js
@@ -39,21 +39,28 @@ function convertAppsArray(aApps, aWindow
function WebappsRegistry() {
}
WebappsRegistry.prototype = {
__proto__: DOMRequestIpcHelper.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":
this.removeMessageListeners("Webapps:Install:Return:KO");
Services.DOMRequest.fireSuccess(req, createContentApplicationObject(this._window, app));
cpmm.sendAsyncMessage("Webapps:Install:Return:Ack",
{ manifestURL : app.manifestURL });
break;
@@ -73,16 +80,36 @@ WebappsRegistry.prototype = {
case "Webapps:CheckInstalled:Return:OK":
this.removeMessageListeners(aMessage.name);
Services.DOMRequest.fireSuccess(req, msg.app);
break;
case "Webapps:GetInstalled:Return:OK":
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);
},
_getOrigin: function(aURL) {
let uri = Services.io.newURI(aURL, null, null);
return uri.prePath;
},
@@ -226,46 +253,97 @@ WebappsRegistry.prototype = {
: this._window.DOMApplicationsManager._create(this._window, mgmt.wrappedJSObject);
}
return this._mgmt;
},
uninit: function() {
this._mgmt = null;
cpmm.sendAsyncMessage("Webapps:UnregisterForMessages",
- ["Webapps:Install:Return:OK"]);
+ ["Webapps:Install:Return:OK",
+ "Webapps:AdditionalLanguageChange"]);
},
installPackage: function(aURL, aParams) {
let request = this.createRequest();
let uri = this._validateURL(aURL, request);
if (uri && this._ensureForeground(request)) {
this.addMessageListeners("Webapps:Install:Return:KO");
cpmm.sendAsyncMessage("Webapps:InstallPackage",
this._prepareInstall(uri, request, aParams, true));
}
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;
let app = appId && appsService.getAppByLocalId(appId);
let isCurrentHomescreen = app &&
app.manifestURL == prefs.get("dom.mozApps.homescreenURL") &&
app.appStatus != Ci.nsIPrincipal.APP_STATUS_NOT_INSTALLED;
--- a/dom/apps/Webapps.jsm
+++ b/dom/apps/Webapps.jsm
@@ -76,16 +76,19 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://gre/modules/WebappOSUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
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");
XPCOMUtils.defineLazyModuleGetter(this, "ImportExport",
"resource://gre/modules/ImportExport.jsm");
#ifdef MOZ_WIDGET_GONK
XPCOMUtils.defineLazyGetter(this, "libcutils", function() {
@@ -238,16 +241,19 @@ this.DOMApplicationRegistry = {
Services.obs.addObserver(this, "memory-pressure", false);
AppDownloadManager.registerCancelFunction(this.cancelDownload.bind(this));
this.appsFile = FileUtils.getFile(DIRECTORY_NAME,
["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.
loadCurrentRegistry: function() {
return AppsUtils.loadJSONAsync(this.appsFile).then((aData) => {
if (!aData) {
return;
}
@@ -419,16 +425,17 @@ this.DOMApplicationRegistry = {
let localeManifest = new ManifestHelper(aResult.manifest, app.origin, app.manifestURL);
this._saveWidgetsFullPath(localeManifest, app);
if (app.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) {
app.redirects = this.sanitizeRedirects(aResult.redirects);
}
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.
this.notifyAppsRegistryReady();
}
}),
updateDataStoreForApp: Task.async(function*(aId) {
@@ -1144,16 +1151,17 @@ this.DOMApplicationRegistry = {
if (app.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) {
app.redirects = this.sanitizeRedirects(manifest.redirects);
}
app.kind = this.appKind(app, aResult.manifest);
this._registerSystemMessages(manifest, app);
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);
});
},
observe: function(aSubject, aTopic, aData) {
if (aTopic == "xpcom-shutdown") {
@@ -1515,16 +1523,18 @@ this.DOMApplicationRegistry = {
let res = {};
let done = false;
// We allow cloning the registry when the local processing has been done.
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) {
manifests.forEach((item) => {
res[item.id] = item.manifest;
});
done = true;
}
@@ -1959,16 +1969,20 @@ this.DOMApplicationRegistry = {
}
delete app.staged;
}
delete app.retryingDownload;
// 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);
let updateManifest = yield AppsUtils.loadJSONAsync(staged.path);
let appObject = AppsUtils.cloneAppObject(app);
appObject.updateManifest = updateManifest;
this.notifyUpdateHandlers(appObject, newManifest, appFile.path);
@@ -2076,21 +2090,23 @@ this.DOMApplicationRegistry = {
this._registerSystemMessages(aNewManifest, aApp);
this._registerActivities(aNewManifest, aApp, true);
this._registerInterAppConnections(aNewManifest, aApp);
} else {
// Nothing else to do but notifying we're ready.
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) {
debug("checkForUpdate for " + aData.manifestURL);
function sendError(aError) {
debug("checkForUpdate error " + aError);
aData.error = aError;
@@ -3180,16 +3196,19 @@ this.DOMApplicationRegistry = {
app: app,
manifest: aManifest,
manifestURL: aNewApp.manifestURL
});
// 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
});
}),
_nextLocalId: function() {
let id = Services.prefs.getIntPref("dom.mozApps.maxLocalId") + 1;
@@ -4089,16 +4108,17 @@ this.DOMApplicationRegistry = {
// Then notify observers.
Services.obs.notifyObservers(null, "webapps-uninstall", JSON.stringify(aApp));
if (supportSystemMessages()) {
this._unregisterActivities(aApp.manifest, aApp);
}
UserCustomizations.unregister(aApp.manifest, aApp);
+ Langpacks.unregister(aApp, aApp.manifest);
let dir = this._getAppDir(id);
try {
dir.remove(true);
} catch (e) {}
delete this.webapps[id];
--- a/dom/apps/moz.build
+++ b/dom/apps/moz.build
@@ -29,16 +29,17 @@ EXTRA_COMPONENTS += [
'Webapps.manifest',
]
EXTRA_JS_MODULES += [
'AppDownloadManager.jsm',
'AppsServiceChild.jsm',
'FreeSpaceWatcher.jsm',
'InterAppCommService.jsm',
+ 'Langpacks.jsm',
'OfflineCacheInstaller.jsm',
'PermissionsInstaller.jsm',
'PermissionsTable.jsm',
'StoreTrustAnchor.jsm',
'UserCustomizations.jsm',
]
EXTRA_PP_JS_MODULES += [
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/langpack/event.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Langpack Test : event</title>
+ <script>
+var baseURL = "http://mochi.test:8888/tests/dom/apps/tests/langpack/";
+var eventCount = 0;
+
+function languageChanged(evt) {
+ eventCount++;
+ alert(JSON.stringify(evt.detail));
+ if (eventCount == 1) {
+ var req = navigator.mozApps.install(baseURL + "lang2.webapp");
+ }
+}
+
+// Set up the event handler, and install an app.
+function run() {
+ document.addEventListener("additionallanguageschange", languageChanged);
+
+ navigator.mozApps.install(baseURL + "lang1.webapp");
+}
+ </script>
+ </head>
+ <body onload="run()">
+ <h1>Langpack Test : event</h1>
+ </body>
+</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/langpack/fr/app.json
@@ -0,0 +1,1 @@
+{ "hello" : "Bonjour" }
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/langpack/fr/app.properties
@@ -0,0 +1,1 @@
+hello=Bonjour
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/langpack/index.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Langpack Test : getAdditionalLanguages()</title>
+ <script>
+function run() {
+ navigator.mozApps.getAdditionalLanguages().then(languages => {
+ alert(JSON.stringify(languages));
+ });
+}
+ </script>
+ </head>
+ <body onload="run()">
+ <h1>Langpack Test : getAdditionalLanguages()</h1>
+ </body>
+</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/langpack/lang1.webapp
@@ -0,0 +1,14 @@
+{
+ "name": "French locale",
+ "languages-target" : { "app://*.gaiamobile.org/manifest.webapp": "2.2" },
+ "languages-provided": {
+ "fr": {
+ "version": 201411051234,
+ "name": "Français",
+ "apps": {
+ "http://mochi.test:8888/tests/dom/apps/tests/langpack/manifest.webapp": "tests/dom/apps/tests/langpack/fr/"
+ }
+ }
+ },
+ "role" : "langpack"
+}
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/langpack/lang1.webapp^headers^
@@ -0,0 +1,1 @@
+Content-Type: application/manifest+json
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/langpack/lang2.webapp
@@ -0,0 +1,21 @@
+{
+ "name": "German an Polish locales",
+ "languages-target" : { "app://*.gaiamobile.org/manifest.webapp": "2.2" },
+ "languages-provided": {
+ "de": {
+ "version": 201411051234,
+ "name": "Deutsch",
+ "apps": {
+ "http://mochi.test:8888/tests/dom/apps/tests/langpack/manifest.webapp": "tests/dom/apps/tests/langpack/de/"
+ }
+ },
+ "pl": {
+ "version": 201411051234,
+ "name": "Polski",
+ "apps": {
+ "http://mochi.test:8888/tests/dom/apps/tests/langpack/manifest.webapp": "tests/dom/apps/tests/langpack/pl/"
+ }
+ }
+ },
+ "role" : "langpack"
+}
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/langpack/lang2.webapp^headers^
@@ -0,0 +1,1 @@
+Content-Type: application/manifest+json
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/langpack/manifest.webapp
@@ -0,0 +1,3 @@
+{
+ "name": "Localization test app"
+}
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/langpack/manifest.webapp^headers^
@@ -0,0 +1,1 @@
+Content-Type: application/manifest+json
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/langpack/resources.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Langpack Test : resources</title>
+ <script>
+function success(data) {
+ return new Promise(function(resolve, reject) {
+ if (typeof data === "object") {
+ // Read what's inside the blob.
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ alert(e.target.result);
+ resolve();
+ };
+ reader.readAsText(data);
+ } else {
+ alert(data);
+ resolve();
+ }
+ });
+}
+
+function successJSON(data) {
+ return new Promise(function(resolve, reject) {
+ alert(JSON.stringify(data));
+ resolve();
+ });
+}
+
+function error(domError) {
+ return new Promise(function(resolve, reject) {
+ alert(domError.name);
+ resolve();
+ });
+}
+
+// Error: Bad resource.
+function test1() {
+ return navigator.mozApps.getLocalizationResource("fr", "2.2", "./foo.html", "binary")
+ .then(success, error);
+}
+
+// Error: Unknown locale.
+function test2() {
+ return navigator.mozApps.getLocalizationResource("es", "2.2", "./foo.html", "binary")
+ .then(success, error);
+}
+
+// Error: Bad version.
+function test3() {
+ return navigator.mozApps.getLocalizationResource("fr", "2.0", "./foo.html", "binary")
+ .then(success, error);
+}
+
+// Error: Absolute url.
+function test4() {
+ return navigator.mozApps.getLocalizationResource("fr", "2.2", "http://example.com/foo.html", "binary")
+ .then(success, error);
+}
+
+// Ok, binary data.
+function test5() {
+ return navigator.mozApps.getLocalizationResource("fr", "2.2", "./app.properties", "binary")
+ .then(success, error);
+}
+
+// Ok, text data.
+function test6() {
+ return navigator.mozApps.getLocalizationResource("fr", "2.2", "./app.properties", "text")
+ .then(success, error);
+}
+
+// Ok, json data.
+function test7() {
+ return navigator.mozApps.getLocalizationResource("fr", "2.2", "./app.json", "json")
+ .then(successJSON, error);
+}
+
+function run() {
+ test1().then(test2)
+ .then(test3)
+ .then(test4)
+ .then(test5)
+ .then(test6)
+ .then(test7);
+}
+ </script>
+ </head>
+ <body onload="run()">
+ <h1>Langpack Test : resources</h1>
+ </body>
+</html>
\ No newline at end of file
--- a/dom/apps/tests/mochitest.ini
+++ b/dom/apps/tests/mochitest.ini
@@ -17,32 +17,34 @@ support-files =
file_trusted_app.template.webapp
file_invalidWidget_app.template.webapp
file_packaged_app.sjs
file_packaged_app.template.html
file_packaged_app.template.webapp
file_widget_app.template.webapp
file_widget_app.template.html
file_test_widget.js
+ langpack/*
signed_app.sjs
signed_app_template.webapp
signed/*
test_packaged_app_common.js
marketplace/*
pkg_install_iframe.html
[test_app_addons.html]
skip-if = os == "android" || toolkit == "gonk" # embed-apps doesn't work in mochitest app
[test_app_enabled.html]
[test_app_update.html]
skip-if = os == "android" || toolkit == "gonk" # embed-apps doesn't work in mochitest app
[test_bug_795164.html]
[test_import_export.html]
[test_install_multiple_apps_origin.html]
[test_install_receipts.html]
+[test_langpacks.html]
[test_marketplace_pkg_install.html]
skip-if = buildapp == "b2g" || toolkit == "android" # see bug 989806
[test_packaged_app_install.html]
skip-if = (toolkit == 'android' && processor == 'x86') #x86 only
[test_packaged_app_update.html]
skip-if = (toolkit == 'android' && processor == 'x86') #x86 only
[test_receipt_operations.html]
[test_signed_pkg_install.html]
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/test_langpacks.html
@@ -0,0 +1,221 @@
+<!DOCTYPE HTML><!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1108096
+-->
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1108096 - Langpack support</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="application/javascript;version=1.7">
+/**
+ * Test for Bug 1108096
+ * This file covers testing langpacks.
+ *
+ * The setup is as follows:
+ * - app is the localizable application.
+ * - langpack1 provides the French locale.
+ * - langpack2 provides the German and Polish locales.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+const uriPrefix = "http://mochi.test:8888/tests/dom/apps/tests/langpack/";
+
+let appManifestURL = uriPrefix + "manifest.webapp";
+let lang1ManifestURL = uriPrefix + "lang1.webapp";
+let lang2ManifestURL = uriPrefix + "lang2.webapp";
+
+let gGenerator = runTest();
+
+function go() {
+ gGenerator.next();
+}
+
+function continueTest() {
+ try {
+ gGenerator.next();
+ } catch (e if e instanceof StopIteration) {
+ SimpleTest.finish();
+ }
+}
+
+function mozAppsError() {
+ ok(false, "mozApps error: " + this.error.name);
+ SimpleTest.finish();
+}
+
+// Triggers one navigation test to the given page.
+// Waits for alert() messages before tearing down the iframe.
+function openPage(pageURL, messages) {
+ info("Navigating to " + pageURL);
+ let ifr = document.createElement("iframe");
+ let listener = function(event) {
+ let message = messages.shift();
+ is(event.detail.message, message, "Checking alert message for " + pageURL);
+ if (messages.length == 0) {
+ ifr.removeEventListener("mozbrowsershowmodalprompt", listener);
+ ifr.parentNode.removeChild(ifr);
+ continueTest();
+ }
+ }
+
+ ifr.addEventListener("mozbrowsershowmodalprompt", listener, false);
+
+ // Open the app url in an iframe.
+ ifr.setAttribute("mozapp", appManifestURL);
+ ifr.setAttribute("mozbrowser", "true");
+ ifr.setAttribute("src", uriPrefix + pageURL);
+ document.getElementById("container").appendChild(ifr);
+}
+
+let apps = [];
+
+function installApp(manifestURL) {
+ info("About to install app at " + manifestURL);
+ let req = navigator.mozApps.install(manifestURL);
+ req.onsuccess = function() {
+ is(req.result.manifestURL, manifestURL, "app installed");
+ if (req.result.installState == "installed") {
+ is(req.result.installState, "installed", "app downloaded");
+ continueTest();
+ } else {
+ req.result.ondownloadapplied = function() {
+ is(req.result.installState, "installed", "app downloaded");
+ continueTest();
+ }
+ }
+ }
+ req.onerror = mozAppsError;
+}
+
+function runTest() {
+ // Set up.
+ SpecialPowers.setAllAppsLaunchable(true);
+ SpecialPowers.pushPrefEnv({'set': [
+ ["dom.mozBrowserFramesEnabled", true],
+ ["dom.apps.allow_unsigned_langpacks", true] ]},continueTest);
+ yield undefined;
+
+ SpecialPowers.pushPermissions(
+ [{ "type": "webapps-manage", "allow": 1, "context": document },
+ { "type": "embed-apps", "allow": 1, "context": document },
+ { "type": "browser", "allow": 1, "context": document } ],
+ continueTest);
+ yield undefined;
+
+ navigator.mozApps.mgmt.oninstall = function(evt) {
+ apps.push(evt.application);
+ };
+
+ let _ = JSON.stringify;
+
+ SpecialPowers.autoConfirmAppInstall(continueTest);
+ yield undefined;
+
+ SpecialPowers.autoConfirmAppUninstall(continueTest);
+ yield undefined;
+
+ // Install test app.
+ installApp(appManifestURL);
+ yield undefined;
+
+ // Opens the iframe to the test page, initial state.
+ // No locale is available.
+ openPage("index.html", [_({})]);
+ yield undefined;
+
+ // Install the fr langpack.
+ installApp(lang1ManifestURL);
+ yield undefined;
+
+ // Opens the iframe to the test page.
+ // Only the French locale is available.
+ openPage("index.html",
+ [_({"fr":[{"version":201411051234,"name":"Français","target":"2.2"}]})]);
+ yield undefined;
+
+ // Install the de and pl langpack.
+ installApp(lang2ManifestURL);
+ yield undefined;
+
+ // Opens the iframe to the test page.
+ // French, German and Polish locales are available.
+ openPage("index.html",
+ [_({"fr":[{"version":201411051234,"name":"Français","target":"2.2"}],"de":[{"version":201411051234,"name":"Deutsch","target":"2.2"}],"pl":[{"version":201411051234,"name":"Polski","target":"2.2"}]})]);
+ yield undefined;
+
+ // Uninstall the second langpack.
+ {
+ let app = apps.pop();
+ info("Uninstalling " + app.manifestURL);
+ req = navigator.mozApps.mgmt.uninstall(app);
+ req.onsuccess = continueTest;
+ req.onerror = mozAppsError;
+ yield undefined;
+ }
+
+ // Opens the iframe to the test page.
+ // Only the French locale is available.
+ openPage("index.html",
+ [_({"fr":[{"version":201411051234,"name":"Français","target":"2.2"}]})]);
+ yield undefined;
+
+ // Uninstall the first langpack.
+ {
+ let app = apps.pop();
+ info("Uninstalling " + app.manifestURL);
+ req = navigator.mozApps.mgmt.uninstall(app);
+ req.onsuccess = continueTest;
+ req.onerror = mozAppsError;
+ yield undefined;
+ }
+
+ // Opens the iframe to the test page, initial state.
+ // No locale is available.
+ openPage("index.html",
+ ["{}"]);
+ yield undefined;
+
+ // Opens the iframe to the event test page.
+ // Will get additionallanguageschange events.
+ openPage("event.html",
+ [_({"fr":[{"version":201411051234,"name":"Français","target":"2.2"}]}),
+ _({"fr":[{"version":201411051234,"name":"Français","target":"2.2"}],"de":[{"version":201411051234,"name":"Deutsch","target":"2.2"}]}),
+ _({"fr":[{"version":201411051234,"name":"Français","target":"2.2"}],"de":[{"version":201411051234,"name":"Deutsch","target":"2.2"}],"pl":[{"version":201411051234,"name":"Polski","target":"2.2"}]})]);
+ yield undefined;
+
+ // Opens the iframe to the resource test page.
+ openPage("resources.html",
+ ["UnavailableResource",
+ "UnavailableLanguage",
+ "UnavailableVersion",
+ "BadUrl",
+ "hello=Bonjour",
+ "hello=Bonjour",
+ _({"hello":"Bonjour"})]);
+ yield undefined;
+
+ // Clean up after ourselves by uninstalling apps.
+ info(apps.length + " applications to uninstall.");
+ while (apps.length) {
+ let app = apps.pop();
+ req = navigator.mozApps.mgmt.uninstall(app);
+ req.onsuccess = continueTest;
+ req.onerror = mozAppsError;
+ yield undefined;
+ }
+}
+
+ </script>
+ </head>
+<body onload="go()">
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+<div id="container"></div>
+</body>
+</html>
--- a/dom/tests/mochitest/webapps/test_list_api.xul
+++ b/dom/tests/mochitest/webapps/test_list_api.xul
@@ -15,17 +15,19 @@
<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=741549"
target="_blank">Mozilla Bug 741549</a>
</body>
<script>
var props = {
checkInstalled: "function",
+ getAdditionalLanguages: "function",
getInstalled: "function",
+ getLocalizationResource: "function",
getSelf: "function",
install: "function",
installPackage: "function",
mgmt: "object",
};
isDeeply([p for (p in navigator.mozApps)].sort(), Object.keys(props).sort(),
"navigator.mozApps has only the expected properties");
--- a/dom/webidl/Apps.webidl
+++ b/dom/webidl/Apps.webidl
@@ -4,26 +4,49 @@
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
dictionary InstallParameters {
sequence<DOMString> receipts = [];
sequence<DOMString> categories = [];
};
+dictionary LanguageDesc {
+ DOMString target;
+ DOMString version;
+ DOMString name;
+};
+
+enum LocaleResourceType {
+ "binary",
+ "json",
+ "text"
+};
+
[NoInterfaceObject, NavigatorProperty="mozApps",
JSImplementation="@mozilla.org/webapps;1"]
interface DOMApplicationsRegistry {
[CheckPermissions="webapps-manage"]
readonly attribute DOMApplicationsManager mgmt;
DOMRequest install(DOMString url, optional InstallParameters params);
DOMRequest installPackage(DOMString url, optional InstallParameters params);
DOMRequest getSelf();
DOMRequest getInstalled();
DOMRequest checkInstalled(DOMString manifestUrl);
+
+ // Language pack API.
+ // These promises will be rejected if the page is not in an app context,
+ // i.e. they are implicitely acting on getSelf().
+ Promise<MozMap<sequence<LanguageDesc>>> getAdditionalLanguages();
+ // Resolves to a different object depending on the dataType value.
+ Promise<any>
+ getLocalizationResource(DOMString language,
+ DOMString version,
+ DOMString path,
+ LocaleResourceType dataType);
};
[JSImplementation="@mozilla.org/webapps/application;1", ChromeOnly]
interface DOMApplication : EventTarget {
// manifest and updateManifest will be turned into dictionaries once
// in bug 1053033 once bug 963382 is fixed.
readonly attribute any manifest;
readonly attribute any updateManifest;
@@ -66,28 +89,28 @@ interface DOMApplication : EventTarget {
DOMRequest checkForUpdate();
/**
* Inter-App Communication APIs.
*
* https://wiki.mozilla.org/WebAPI/Inter_App_Communication_Alt_proposal
*
*/
- Promise<MozInterAppConnection> connect(DOMString keyword, optional any rules);
+ Promise<MozInterAppConnection> connect(DOMString keyword, optional any rules);
- Promise<sequence<MozInterAppMessagePort>> getConnections();
+ Promise<sequence<MozInterAppMessagePort>> getConnections();
- // Receipts handling functions.
- DOMRequest addReceipt(optional DOMString receipt);
- DOMRequest removeReceipt(optional DOMString receipt);
- DOMRequest replaceReceipt(optional DOMString oldReceipt,
- optional DOMString newReceipt);
+ // Receipts handling functions.
+ DOMRequest addReceipt(optional DOMString receipt);
+ DOMRequest removeReceipt(optional DOMString receipt);
+ DOMRequest replaceReceipt(optional DOMString oldReceipt,
+ optional DOMString newReceipt);
- // Export this app as a shareable Blob.
- Promise<Blob> export();
+ // Export this app as a shareable Blob.
+ Promise<Blob> export();
};
[JSImplementation="@mozilla.org/webapps/manager;1",
ChromeOnly,
CheckPermissions="webapps-manage"]
interface DOMApplicationsManager : EventTarget {
DOMRequest getAll();
DOMRequest getNotInstalled();