Bumping manifests a=b2g-bump
[gecko.git] / dom / apps / Langpacks.jsm
blob844d1d00251c80f2492366d528a86b2797789d99
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/. */
5 "use strict";
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")
22   ? (aMsg) => {
23       dump("-*-*- Langpacks: " + aMsg + "\n");
24     }
25   : (aMsg) => {};
27 /**
28   * Langpack support
29   *
30   * Manifest format is:
31   *
32   * "languages-target" : { "app://*.gaiamobile.org/manifest.webapp": "2.2" },
33   * "languages-provided": {
34   * "de": {
35   *   "revision": 201411051234,
36   *   "name": "Deutsch",
37   *   "apps": {
38   *     "app://calendar.gaiamobile.org/manifest.webapp": "/de/calendar",
39   *     "app://email.gaiamobile.org/manifest.webapp": "/de/email"
40   *    }
41   *  },
42   *  "role" : "langpack"
43   */
45 this.Langpacks = {
47   _data: {},
48   _broadcaster: null,
49   _appIdFromManifestURL: null,
50   _appFromManifestURL: null,
52   init: function() {
53     ppmm.addMessageListener("Webapps:GetLocalizationResource", this);
54     ppmm.addMessageListener("Webapps:GetLocalizedValue", this);
55   },
57   registerRegistryFunctions: function(aBroadcaster, aIdGetter, aAppGetter) {
58     this._broadcaster = aBroadcaster;
59     this._appIdFromManifestURL = aIdGetter;
60     this._appFromManifestURL = aAppGetter;
61   },
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);
69         break;
70       case "Webapps:GetLocalizedValue":
71         this.getLocalizedValue(data, mm);
72         break;
73       default:
74         debug("Unexpected message: " + aMessage.name);
75     }
76   },
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) {
85         if (!langs[lang]) {
86           langs[lang] = [];
87         }
88         let current = this._data[aManifestURL].langs[lang];
89         langs[lang].push({
90           revision: current.revision,
91           name: current.name,
92           target: current.target
93         });
94       }
95     }
96     debug("Languages found: " + uneval(res));
97     return res;
98   },
100   sendAppUpdate: function(aManifestURL) {
101     debug("sendAppUpdate " + aManifestURL);
102     if (!this._broadcaster) {
103       debug("No broadcaster!");
104       return;
105     }
107     let res = this.getAdditionalLanguages(aManifestURL);
108     let message = {
109       id: res.appId,
110       app: {
111         additionalLanguages: res.langs
112       }
113     }
114     this._broadcaster("Webapps:UpdateState", message);
115   },
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";
130     }
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);
137         } else {
138           aReject();
139         }
140       });
141       xhr.addEventListener("error", aReject);
142       xhr.send(null);
143     });
144   },
146   getLocalizationResource: function(aData, aMm) {
147     debug("getLocalizationResource " + uneval(aData));
149     function sendError(aMsg, aCode) {
150       debug(aMsg);
151       aMm.sendAsyncMessage("Webapps:GetLocalizationResource:Return",
152         { requestID: aData.requestID, oid: aData.oid, error: aCode });
153     }
155     // No langpack available for this app.
156     if (!this._data[aData.manifestURL]) {
157       return sendError("No langpack for this app.", "NoLangpack");
158     }
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");
164     }
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");
171     }
173     // The path can't be an absolute uri.
174     if (isAbsoluteURI(aData.path)) {
175       return sendError("url can't be absolute.", "BadUrl");
176     }
178     let href = item.url + aData.path;
179     debug("Will load " + href);
181     this._getResource(href, aData.dataType).then(
182       (aResponse) => {
183         aMm.sendAsyncMessage("Webapps:GetLocalizationResource:Return",
184           { requestID: aData.requestID, oid: aData.oid, data: aResponse });
185       },
186       () => { sendError("Error loading " + href, "UnavailableResource"); }
187     );
188   },
190   getLocalizedValue: function(aData, aMm) {
191     debug("getLocalizedValue " + aData.property);
192     function sendError(aMsg, aCode) {
193       debug(aMsg);
194       aMm.sendAsyncMessage("Webapps:GetLocalizedValue:Return",
195         { success: false,
196           requestID: aData.requestID,
197           oid: aData.oid,
198           error: aCode });
199     }
201     function getValueFromManifest(aManifest) {
202       debug("Getting " + aData.property + " from the manifest.");
203       let value = aManifest._localeProp(aData.property);
204       if (!value) {
205         sendError("No property " + aData.property + " in manifest", "UnknownProperty");
206       } else {
207         aMm.sendAsyncMessage("Webapps:GetLocalizedValue:Return",
208         { success: true,
209           requestID: aData.requestID,
210           oid: aData.oid,
211           value: value });
212       }
213     }
215     let self = this;
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]
225           : aResponse;
226         return root[aProp];
227       }
229       self._getResource(href, "json").then(
230         (aResponse) => {
231           let propValue = getProperty(aResponse, aData.property);
232           if (propValue) {
233             aMm.sendAsyncMessage("Webapps:GetLocalizedValue:Return",
234               { success: true,
235                 requestID: aData.requestID,
236                 oid: aData.oid,
237                 value: propValue });
238           } else {
239             getValueFromManifest(aManifest);
240           }
241         },
242         () => { getValueFromManifest(aManifest); }
243       );
244     }
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)
249       .then(aApp => {
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);
258           return;
259         }
261         if (!manifest.version) {
262           getValueFromManifest(manifest);
263           return;
264         }
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);
272           return;
273         }
274         // Fallback on getting the value from the manifest.
275         getValueFromManifest(manifest);
276       })
277       .catch(aError => { sendError("No app!", "NoSuchApp") });
278   },
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.")
284       return false;
285     }
287     if (!("languages-provided" in aManifest)) {
288       debug("Error: no 'languages-provided' property.")
289       return false;
290     }
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);
297         return false;
298       }
300       if (typeof item.revision !== "number") {
301         debug("Error: languages-provided." + lang +
302               ".revision must be a number but is a " + (typeof item.revision));
303         return false;
304       }
306       if (!item.apps) {
307         debug("Error: missing 'apps' in languages-provided." + lang);
308         return false;
309       }
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.");
316           return false;
317         }
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]);
323           return false;
324         }
325       }
326     }
327     return true;
328   },
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.
337       return;
338     }
340     if (!this.checkManifest(aManifest)) {
341       debug("Invalid langpack manifest.");
342       return;
343     }
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]) {
359             this._data[app] = {
360               appId: this._appIdFromManifestURL(app),
361               langs: {}
362             };
363           }
364           this._data[app].langs[lang] = {
365             revision: revision,
366             target: platformVersion,
367             name: name,
368             url: origin.resolve(item.apps[app]),
369             from: aApp.manifestURL
370           }
371           sendEvent = true;
372           debug("Registered " + app + " -> " + uneval(this._data[app].langs[lang]));
373         }
375         // Fire additionallanguageschange event.
376         // This will only be dispatched to documents using the langpack api.
377         if (sendEvent) {
378           this.sendAppUpdate(app);
379           ppmm.broadcastAsyncMessage(
380             "Webapps:AdditionalLanguageChange",
381             { manifestURL: app,
382               languages: this.getAdditionalLanguages(app).langs });
383         }
384       }
385     }
386   },
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.
396         return;
397       }
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) {
403             sendEvent = true;
404             delete this._data[app].langs[lang];
405           }
406         }
407         // Fire additionallanguageschange event.
408         // This will only be dispatched to documents using the langpack api.
409         if (sendEvent) {
410           this.sendAppUpdate(app);
411           ppmm.broadcastAsyncMessage(
412               "Webapps:AdditionalLanguageChange",
413               { manifestURL: app,
414                 languages: this.getAdditionalLanguages(app).langs });
415         }
416       }
417   }
420 Langpacks.init();