1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 const Cu = Components.utils;
8 const Cc = Components.classes;
9 const Ci = Components.interfaces;
11 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
12 Cu.import("resource://gre/modules/Services.jsm");
13 Cu.import("resource://gre/modules/AppsUtils.jsm");
15 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
16 "@mozilla.org/parentprocessmessagemanager;1",
17 "nsIMessageBroadcaster");
19 this.EXPORTED_SYMBOLS = ["Langpacks"];
21 let debug = Services.prefs.getBoolPref("dom.mozApps.debug")
23 dump("-*-*- Langpacks: " + aMsg + "\n");
32 * "languages-target" : { "app://*.gaiamobile.org/manifest.webapp": "2.2" },
33 * "languages-provided": {
35 * "revision": 201411051234,
38 * "app://calendar.gaiamobile.org/manifest.webapp": "/de/calendar",
39 * "app://email.gaiamobile.org/manifest.webapp": "/de/email"
49 _appIdFromManifestURL: null,
50 _appFromManifestURL: null,
53 ppmm.addMessageListener("Webapps:GetLocalizationResource", this);
54 ppmm.addMessageListener("Webapps:GetLocalizedValue", this);
57 registerRegistryFunctions: function(aBroadcaster, aIdGetter, aAppGetter) {
58 this._broadcaster = aBroadcaster;
59 this._appIdFromManifestURL = aIdGetter;
60 this._appFromManifestURL = aAppGetter;
63 receiveMessage: function(aMessage) {
64 let data = aMessage.data;
65 let mm = aMessage.target;
66 switch (aMessage.name) {
67 case "Webapps:GetLocalizationResource":
68 this.getLocalizationResource(data, mm);
70 case "Webapps:GetLocalizedValue":
71 this.getLocalizedValue(data, mm);
74 debug("Unexpected message: " + aMessage.name);
78 getAdditionalLanguages: function(aManifestURL) {
79 debug("getAdditionalLanguages " + aManifestURL);
80 let res = { langs: {} };
81 let langs = res.langs;
82 if (this._data[aManifestURL]) {
83 res.appId = this._data[aManifestURL].appId;
84 for (let lang in this._data[aManifestURL].langs) {
88 let current = this._data[aManifestURL].langs[lang];
90 revision: current.revision,
92 target: current.target
96 debug("Languages found: " + uneval(res));
100 sendAppUpdate: function(aManifestURL) {
101 debug("sendAppUpdate " + aManifestURL);
102 if (!this._broadcaster) {
103 debug("No broadcaster!");
107 let res = this.getAdditionalLanguages(aManifestURL);
111 additionalLanguages: res.langs
114 this._broadcaster("Webapps:UpdateState", message);
117 _getResource: function(aURL, aResponseType) {
118 let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
119 .createInstance(Ci.nsIXMLHttpRequest);
120 xhr.mozBackgroundRequest = true;
121 xhr.open("GET", aURL);
123 // Default to text response type, but the webidl binding takes care of
124 // validating the dataType value.
125 xhr.responseType = "text";
126 if (aResponseType === "json") {
127 xhr.responseType = "json";
128 } else if (aResponseType === "binary") {
129 xhr.responseType = "blob";
132 return new Promise((aResolve, aReject) => {
133 xhr.addEventListener("load", function() {
134 debug("Success loading " + aURL);
135 if (xhr.status >= 200 && xhr.status < 400) {
136 aResolve(xhr.response);
141 xhr.addEventListener("error", aReject);
146 getLocalizationResource: function(aData, aMm) {
147 debug("getLocalizationResource " + uneval(aData));
149 function sendError(aMsg, aCode) {
151 aMm.sendAsyncMessage("Webapps:GetLocalizationResource:Return",
152 { requestID: aData.requestID, oid: aData.oid, error: aCode });
155 // No langpack available for this app.
156 if (!this._data[aData.manifestURL]) {
157 return sendError("No langpack for this app.", "NoLangpack");
160 // We have langpack(s) for this app, but not for this language.
161 if (!this._data[aData.manifestURL].langs[aData.lang]) {
162 return sendError("No language " + aData.lang + " for this app.",
163 "UnavailableLanguage");
166 // Check that we have the langpack for the right app version.
167 let item = this._data[aData.manifestURL].langs[aData.lang];
168 if (item.target != aData.version) {
169 return sendError("No version " + aData.version + " for this app.",
170 "UnavailableVersion");
173 // The path can't be an absolute uri.
174 if (isAbsoluteURI(aData.path)) {
175 return sendError("url can't be absolute.", "BadUrl");
178 let href = item.url + aData.path;
179 debug("Will load " + href);
181 this._getResource(href, aData.dataType).then(
183 aMm.sendAsyncMessage("Webapps:GetLocalizationResource:Return",
184 { requestID: aData.requestID, oid: aData.oid, data: aResponse });
186 () => { sendError("Error loading " + href, "UnavailableResource"); }
190 getLocalizedValue: function(aData, aMm) {
191 debug("getLocalizedValue " + aData.property);
192 function sendError(aMsg, aCode) {
194 aMm.sendAsyncMessage("Webapps:GetLocalizedValue:Return",
196 requestID: aData.requestID,
201 function getValueFromManifest(aManifest) {
202 debug("Getting " + aData.property + " from the manifest.");
203 let value = aManifest._localeProp(aData.property);
205 sendError("No property " + aData.property + " in manifest", "UnknownProperty");
207 aMm.sendAsyncMessage("Webapps:GetLocalizedValue:Return",
209 requestID: aData.requestID,
217 function getValueFromLangpack(aItem, aManifest) {
218 debug("Getting value from langpack at " + aItem.url + "/manifest.json")
219 let href = aItem.url + "/manifest.json";
221 function getProperty(aResponse, aProp) {
222 let root = aData.entryPoint && aResponse.entry_points &&
223 aResponse.entry_points[aData.entryPoint]
224 ? aResponse.entry_points[aData.entryPoint]
229 self._getResource(href, "json").then(
231 let propValue = getProperty(aResponse, aData.property);
233 aMm.sendAsyncMessage("Webapps:GetLocalizedValue:Return",
235 requestID: aData.requestID,
239 getValueFromManifest(aManifest);
242 () => { getValueFromManifest(aManifest); }
246 // We need to get the app with the manifest since the version is only
247 // available in the manifest.
248 this._appFromManifestURL(aData.manifestURL, aData.entryPoint, aData.lang)
250 let manifest = aApp.manifest;
252 // No langpack for this app or we have langpack(s) for this app, but
253 // not for this language.
254 // Fallback to the manifest values.
255 if (!this._data[aData.manifestURL] ||
256 !this._data[aData.manifestURL].langs[aData.lang]) {
257 getValueFromManifest(manifest);
261 if (!manifest.version) {
262 getValueFromManifest(manifest);
266 // Check that we have the langpack for the right app version.
267 let item = this._data[aData.manifestURL].langs[aData.lang];
268 // Only keep x.y in the manifest's version in case it's x.y.z
269 let manVersion = manifest.version.split('.').slice(0, 2).join('.');
270 if (item.target == manVersion) {
271 getValueFromLangpack(item, manifest);
274 // Fallback on getting the value from the manifest.
275 getValueFromManifest(manifest);
277 .catch(aError => { sendError("No app!", "NoSuchApp") });
280 // Validates the langpack part of a manifest.
281 checkManifest: function(aManifest) {
282 if (!("languages-target" in aManifest)) {
283 debug("Error: no 'languages-target' property.")
287 if (!("languages-provided" in aManifest)) {
288 debug("Error: no 'languages-provided' property.")
292 for (let lang in aManifest["languages-provided"]) {
293 let item = aManifest["languages-provided"][lang];
295 if (!item.revision) {
296 debug("Error: missing 'revision' in languages-provided." + lang);
300 if (typeof item.revision !== "number") {
301 debug("Error: languages-provided." + lang +
302 ".revision must be a number but is a " + (typeof item.revision));
307 debug("Error: missing 'apps' in languages-provided." + lang);
311 for (let app in item.apps) {
312 // Keys should be manifest urls, ie. absolute urls.
313 if (!isAbsoluteURI(app)) {
314 debug("Error: languages-provided." + lang + "." + app +
315 " must be an absolute manifest url.");
319 if (typeof item.apps[app] !== "string") {
320 debug("Error: languages-provided." + lang + ".apps." + app +
321 " value must be a string but is " + (typeof item.apps[app]) +
322 " : " + item.apps[app]);
330 // Check if this app is a langpack and update registration if needed.
331 register: function(aApp, aManifest) {
332 debug("register app " + aApp.manifestURL + " role=" + aApp.role);
334 if (aApp.role !== "langpack") {
335 debug("Not a langpack.");
336 // Not a langpack, but that's fine.
340 if (!this.checkManifest(aManifest)) {
341 debug("Invalid langpack manifest.");
345 let platformVersion = aManifest["languages-target"]
346 ["app://*.gaiamobile.org/manifest.webapp"];
347 let origin = Services.io.newURI(aApp.origin, null, null);
349 for (let lang in aManifest["languages-provided"]) {
350 let item = aManifest["languages-provided"][lang];
351 let revision = item.revision;
352 let name = item.name || lang; // If no name specified, default to lang.
353 for (let app in item.apps) {
354 let sendEvent = false;
355 if (!this._data[app] ||
356 !this._data[app].langs[lang] ||
357 this._data[app].langs[lang].revision > revision) {
358 if (!this._data[app]) {
360 appId: this._appIdFromManifestURL(app),
364 this._data[app].langs[lang] = {
366 target: platformVersion,
368 url: origin.resolve(item.apps[app]),
369 from: aApp.manifestURL
372 debug("Registered " + app + " -> " + uneval(this._data[app].langs[lang]));
375 // Fire additionallanguageschange event.
376 // This will only be dispatched to documents using the langpack api.
378 this.sendAppUpdate(app);
379 ppmm.broadcastAsyncMessage(
380 "Webapps:AdditionalLanguageChange",
382 languages: this.getAdditionalLanguages(app).langs });
388 // Check if this app is a langpack and update registration by removing all
389 // the entries from this app.
390 unregister: function(aApp, aManifest) {
391 debug("unregister app " + aApp.manifestURL + " role=" + aApp.role);
393 if (aApp.role !== "langpack") {
394 debug("Not a langpack.");
395 // Not a langpack, but that's fine.
399 for (let app in this._data) {
400 let sendEvent = false;
401 for (let lang in this._data[app].langs) {
402 if (this._data[app].langs[lang].from == aApp.manifestURL) {
404 delete this._data[app].langs[lang];
407 // Fire additionallanguageschange event.
408 // This will only be dispatched to documents using the langpack api.
410 this.sendAppUpdate(app);
411 ppmm.broadcastAsyncMessage(
412 "Webapps:AdditionalLanguageChange",
414 languages: this.getAdditionalLanguages(app).langs });