Bumping manifests a=b2g-bump
[gecko.git] / dom / payment / Payment.jsm
blobf78037ef90c295eb241bf93346abdf327718c33e
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 {classes: Cc, interfaces: Ci, utils: Cu} = Components;
9 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
10 Cu.import("resource://gre/modules/Services.jsm");
12 this.EXPORTED_SYMBOLS = [];
14 const PAYMENT_IPC_MSG_NAMES = ["Payment:Pay",
15                                "Payment:Success",
16                                "Payment:Failed"];
18 const PREF_PAYMENTPROVIDERS_BRANCH = "dom.payment.provider.";
19 const PREF_PAYMENT_BRANCH = "dom.payment.";
20 const PREF_DEBUG = "dom.payment.debug";
22 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
23                                    "@mozilla.org/parentprocessmessagemanager;1",
24                                    "nsIMessageListenerManager");
26 XPCOMUtils.defineLazyServiceGetter(this, "prefService",
27                                    "@mozilla.org/preferences-service;1",
28                                    "nsIPrefService");
30 let PaymentManager =  {
31   init: function init() {
32     // Payment providers data are stored as a preference.
33     this.registeredProviders = null;
35     this.messageManagers = {};
37     // The dom.payment.skipHTTPSCheck pref is supposed to be used only during
38     // development process. This preference should not be active for a
39     // production build.
40     let paymentPrefs = prefService.getBranch(PREF_PAYMENT_BRANCH);
41     this.checkHttps = true;
42     try {
43       if (paymentPrefs.getPrefType("skipHTTPSCheck")) {
44         this.checkHttps = !paymentPrefs.getBoolPref("skipHTTPSCheck");
45       }
46     } catch(e) {}
48     for each (let msgname in PAYMENT_IPC_MSG_NAMES) {
49       ppmm.addMessageListener(msgname, this);
50     }
52     Services.obs.addObserver(this, "xpcom-shutdown", false);
54     try {
55       this._debug =
56         Services.prefs.getPrefType(PREF_DEBUG) == Ci.nsIPrefBranch.PREF_BOOL
57         && Services.prefs.getBoolPref(PREF_DEBUG);
58     } catch(e) {
59       this._debug = false;
60     }
61   },
63   /**
64    * Process a message from the content process.
65    */
66   receiveMessage: function receiveMessage(aMessage) {
67     let name = aMessage.name;
68     let msg = aMessage.json;
69     if (this._debug) {
70       this.LOG("Received '" + name + "' message from content process");
71     }
73     switch (name) {
74       case "Payment:Pay": {
75         // First of all, we register the payment providers.
76         if (!this.registeredProviders) {
77           this.registeredProviders = {};
78           this.registerPaymentProviders();
79         }
81         // We save the message target message manager so we can later dispatch
82         // back messages without broadcasting to all child processes.
83         let requestId = msg.requestId;
84         this.messageManagers[requestId] = aMessage.target;
86         // We check the jwt type and look for a match within the
87         // registered payment providers to get the correct payment request
88         // information.
89         let paymentRequests = [];
90         let jwtTypes = [];
91         for (let i in msg.jwts) {
92           let pr = this.getPaymentRequestInfo(requestId, msg.jwts[i]);
93           if (!pr) {
94             continue;
95           }
96           // We consider jwt type repetition an error.
97           if (jwtTypes[pr.type]) {
98             this.paymentFailed(requestId,
99                                "PAY_REQUEST_ERROR_DUPLICATED_JWT_TYPE");
100             return;
101           }
102           jwtTypes[pr.type] = true;
103           paymentRequests.push(pr);
104         }
106         if (!paymentRequests.length) {
107           this.paymentFailed(requestId,
108                              "PAY_REQUEST_ERROR_NO_VALID_REQUEST_FOUND");
109           return;
110         }
112         // After getting the list of valid payment requests, we ask the user
113         // for confirmation before sending any request to any payment provider.
114         // If there is more than one choice, we also let the user select the one
115         // that he prefers.
116         let glue = Cc["@mozilla.org/payment/ui-glue;1"]
117                    .createInstance(Ci.nsIPaymentUIGlue);
118         if (!glue) {
119           if (this._debug) {
120             this.LOG("Could not create nsIPaymentUIGlue instance");
121           }
122           this.paymentFailed(requestId,
123                              "INTERNAL_ERROR_CREATE_PAYMENT_GLUE_FAILED");
124           return;
125         }
127         let confirmPaymentSuccessCb = function successCb(aRequestId,
128                                                          aResult) {
129           // Get the appropriate payment provider data based on user's choice.
130           let selectedProvider = this.registeredProviders[aResult];
131           if (!selectedProvider || !selectedProvider.uri) {
132             if (this._debug) {
133               this.LOG("Could not retrieve a valid provider based on user's " +
134                         "selection");
135             }
136             this.paymentFailed(aRequestId,
137                                "INTERNAL_ERROR_NO_VALID_SELECTED_PROVIDER");
138             return;
139           }
141           let jwt;
142           for (let i in paymentRequests) {
143             if (paymentRequests[i].type == aResult) {
144               jwt = paymentRequests[i].jwt;
145               break;
146             }
147           }
148           if (!jwt) {
149             if (this._debug) {
150               this.LOG("The selected request has no JWT information " +
151                         "associated");
152             }
153             this.paymentFailed(aRequestId,
154                                "INTERNAL_ERROR_NO_JWT_ASSOCIATED_TO_REQUEST");
155             return;
156           }
158           this.showPaymentFlow(aRequestId, selectedProvider, jwt);
159         };
161         let confirmPaymentErrorCb = this.paymentFailed;
163         glue.confirmPaymentRequest(requestId,
164                                    paymentRequests,
165                                    confirmPaymentSuccessCb.bind(this),
166                                    confirmPaymentErrorCb.bind(this));
167         break;
168       }
169       case "Payment:Success":
170       case "Payment:Failed": {
171         let mm = this.messageManagers[msg.requestId];
172         mm.sendAsyncMessage(name, {
173           requestId: msg.requestId,
174           result: msg.result,
175           errorMsg: msg.errorMsg
176         });
177         break;
178       }
179     }
180   },
182   /**
183    * Helper function to register payment providers stored as preferences.
184    */
185   registerPaymentProviders: function registerPaymentProviders() {
186     let paymentProviders = prefService
187                            .getBranch(PREF_PAYMENTPROVIDERS_BRANCH)
188                            .getChildList("");
190     // First get the numbers of the providers by getting all ###.uri prefs.
191     let nums = [];
192     for (let i in paymentProviders) {
193       let match = /^(\d+)\.uri$/.exec(paymentProviders[i]);
194       if (!match) {
195         continue;
196       } else {
197         nums.push(match[1]);
198       }
199     }
201 #ifdef MOZ_B2G
202     let appsService = Cc["@mozilla.org/AppsService;1"]
203                         .getService(Ci.nsIAppsService);
204     let systemAppId = Ci.nsIScriptSecurityManager.NO_APP_ID;
206     try {
207       let manifestURL = Services.prefs.getCharPref("b2g.system_manifest_url");
208       systemAppId = appsService.getAppLocalIdByManifestURL(manifestURL);
209       this.LOG("System app id=" + systemAppId);
210     } catch(e) {}
211 #endif
213     // Now register the payment providers.
214     for (let i in nums) {
215       let branch = prefService
216                    .getBranch(PREF_PAYMENTPROVIDERS_BRANCH + nums[i] + ".");
217       let vals = branch.getChildList("");
218       if (vals.length == 0) {
219         return;
220       }
221       try {
222         let type = branch.getCharPref("type");
223         if (type in this.registeredProviders) {
224           continue;
225         }
226         let provider = this.registeredProviders[type] = {
227           name: branch.getCharPref("name"),
228           uri: branch.getCharPref("uri"),
229           description: branch.getCharPref("description"),
230           requestMethod: branch.getCharPref("requestMethod")
231         };
233 #ifdef MOZ_B2G
234         // Let this payment provider access the firefox-accounts API when
235         // it's loaded in the trusted UI.
236         if (systemAppId != Ci.nsIScriptSecurityManager.NO_APP_ID) {
237           this.LOG("Granting firefox-accounts permission to " + provider.uri);
238           let uri = Services.io.newURI(provider.uri, null, null);
239           let principal = Services.scriptSecurityManager
240                             .getAppCodebasePrincipal(uri, systemAppId, true);
242           Services.perms.addFromPrincipal(principal, "firefox-accounts",
243                                           Ci.nsIPermissionManager.ALLOW_ACTION,
244                                           Ci.nsIPermissionManager.EXPIRE_SESSION);
245         }
246 #endif
248         if (this._debug) {
249           this.LOG("Registered Payment Providers: " +
250                     JSON.stringify(this.registeredProviders[type]));
251         }
252       } catch (ex) {
253         if (this._debug) {
254           this.LOG("An error ocurred registering a payment provider. " + ex);
255         }
256       }
257     }
258   },
260   /**
261    * Helper for sending a Payment:Failed message to the parent process.
262    */
263   paymentFailed: function paymentFailed(aRequestId, aErrorMsg) {
264     let mm = this.messageManagers[aRequestId];
265     mm.sendAsyncMessage("Payment:Failed", {
266       requestId: aRequestId,
267       errorMsg: aErrorMsg
268     });
269   },
271   /**
272    * Helper function to get the payment request info according to the jwt
273    * type. Payment provider's data is stored as a preference.
274    */
275   getPaymentRequestInfo: function getPaymentRequestInfo(aRequestId, aJwt) {
276     if (!aJwt) {
277       this.paymentFailed(aRequestId, "INTERNAL_ERROR_CALL_WITH_MISSING_JWT");
278       return true;
279     }
281     // First thing, we check that the jwt type is an allowed type and has a
282     // payment provider flow information associated.
284     // A jwt string consists in three parts separated by period ('.'): header,
285     // payload and signature.
286     let segments = aJwt.split('.');
287     if (segments.length !== 3) {
288       if (this._debug) {
289         this.LOG("Error getting payment provider's uri. " +
290                   "Not enough or too many segments");
291       }
292       this.paymentFailed(aRequestId,
293                          "PAY_REQUEST_ERROR_WRONG_SEGMENTS_COUNT");
294       return true;
295     }
297     let payloadObject;
298     try {
299       // We only care about the payload segment, which contains the jwt type
300       // that should match with any of the stored payment provider's data and
301       // the payment request information to be shown to the user.
302       // Before decoding the JWT string we need to normalize it to be compliant
303       // with RFC 4648.
304       segments[1] = segments[1].replace("-", "+", "g").replace("_", "/", "g");
305       let payload = atob(segments[1]);
306       if (this._debug) {
307         this.LOG("Payload " + payload);
308       }
309       if (!payload.length) {
310         this.paymentFailed(aRequestId, "PAY_REQUEST_ERROR_EMPTY_PAYLOAD");
311         return true;
312       }
313       payloadObject = JSON.parse(payload);
314       if (!payloadObject) {
315         this.paymentFailed(aRequestId,
316                            "PAY_REQUEST_ERROR_ERROR_PARSING_JWT_PAYLOAD");
317         return true;
318       }
319     } catch (e) {
320       this.paymentFailed(aRequestId,
321                          "PAY_REQUEST_ERROR_ERROR_DECODING_JWT");
322       return true;
323     }
325     if (!payloadObject.typ) {
326       this.paymentFailed(aRequestId,
327                          "PAY_REQUEST_ERROR_NO_TYP_PARAMETER");
328       return true;
329     }
331     if (!payloadObject.request) {
332       this.paymentFailed(aRequestId,
333                          "PAY_REQUEST_ERROR_NO_REQUEST_PARAMETER");
334       return true;
335     }
337     // Once we got the jwt 'typ' value we look for a match within the payment
338     // providers stored preferences. If the jwt 'typ' is not recognized as one
339     // of the allowed values for registered payment providers, we skip the jwt
340     // validation but we don't fire any error. This way developers might have
341     // a default set of well formed JWTs that might be used in different B2G
342     // devices with a different set of allowed payment providers.
343     let provider = this.registeredProviders[payloadObject.typ];
344     if (!provider) {
345       if (this._debug) {
346         this.LOG("Not registered payment provider for jwt type: " +
347                   payloadObject.typ);
348       }
349       return false;
350     }
352     if (!provider.uri || !provider.name) {
353       this.paymentFailed(aRequestId,
354                          "INTERNAL_ERROR_WRONG_REGISTERED_PAY_PROVIDER");
355       return true;
356     }
358     // We only allow https for payment providers uris.
359     if (this.checkHttps && !/^https/.exec(provider.uri.toLowerCase())) {
360       // We should never get this far.
361       if (this._debug) {
362         this.LOG("Payment provider uris must be https: " + provider.uri);
363       }
364       this.paymentFailed(aRequestId,
365                          "INTERNAL_ERROR_NON_HTTPS_PROVIDER_URI");
366       return true;
367     }
369     let pldRequest = payloadObject.request;
370     return { jwt: aJwt, type: payloadObject.typ, providerName: provider.name };
371   },
373   showPaymentFlow: function showPaymentFlow(aRequestId,
374                                             aPaymentProvider,
375                                             aJwt) {
376     let paymentFlowInfo = Cc["@mozilla.org/payment/flow-info;1"]
377                           .createInstance(Ci.nsIPaymentFlowInfo);
378     paymentFlowInfo.uri = aPaymentProvider.uri;
379     paymentFlowInfo.requestMethod = aPaymentProvider.requestMethod;
380     paymentFlowInfo.jwt = aJwt;
381     paymentFlowInfo.name = aPaymentProvider.name;
382     paymentFlowInfo.description = aPaymentProvider.description;
384     let glue = Cc["@mozilla.org/payment/ui-glue;1"]
385                .createInstance(Ci.nsIPaymentUIGlue);
386     if (!glue) {
387       if (this._debug) {
388         this.LOG("Could not create nsIPaymentUIGlue instance");
389       }
390       this.paymentFailed(aRequestId,
391                          "INTERNAL_ERROR_CREATE_PAYMENT_GLUE_FAILED");
392       return false;
393     }
394     glue.showPaymentFlow(aRequestId,
395                          paymentFlowInfo,
396                          this.paymentFailed.bind(this));
397   },
399   // nsIObserver
401   observe: function observe(subject, topic, data) {
402     if (topic == "xpcom-shutdown") {
403       for each (let msgname in PAYMENT_IPC_MSG_NAMES) {
404         ppmm.removeMessageListener(msgname, this);
405       }
406       this.registeredProviders = null;
407       this.messageManagers = null;
409       Services.obs.removeObserver(this, "xpcom-shutdown");
410     }
411   },
413   LOG: function LOG(s) {
414     if (!this._debug) {
415       return;
416     }
417     dump("-*- PaymentManager: " + s + "\n");
418   }
421 PaymentManager.init();