|
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/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 const {classes: Cc, interfaces: Ci, utils: Cu} = Components; |
|
8 |
|
9 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
10 Cu.import("resource://gre/modules/Services.jsm"); |
|
11 |
|
12 this.EXPORTED_SYMBOLS = []; |
|
13 |
|
14 const PAYMENT_IPC_MSG_NAMES = ["Payment:Pay", |
|
15 "Payment:Success", |
|
16 "Payment:Failed"]; |
|
17 |
|
18 const PREF_PAYMENTPROVIDERS_BRANCH = "dom.payment.provider."; |
|
19 const PREF_PAYMENT_BRANCH = "dom.payment."; |
|
20 const PREF_DEBUG = "dom.payment.debug"; |
|
21 |
|
22 XPCOMUtils.defineLazyServiceGetter(this, "ppmm", |
|
23 "@mozilla.org/parentprocessmessagemanager;1", |
|
24 "nsIMessageListenerManager"); |
|
25 |
|
26 XPCOMUtils.defineLazyServiceGetter(this, "prefService", |
|
27 "@mozilla.org/preferences-service;1", |
|
28 "nsIPrefService"); |
|
29 |
|
30 let PaymentManager = { |
|
31 init: function init() { |
|
32 // Payment providers data are stored as a preference. |
|
33 this.registeredProviders = null; |
|
34 |
|
35 this.messageManagers = {}; |
|
36 |
|
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) {} |
|
47 |
|
48 for each (let msgname in PAYMENT_IPC_MSG_NAMES) { |
|
49 ppmm.addMessageListener(msgname, this); |
|
50 } |
|
51 |
|
52 Services.obs.addObserver(this, "xpcom-shutdown", false); |
|
53 |
|
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 }, |
|
62 |
|
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 } |
|
72 |
|
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 } |
|
80 |
|
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; |
|
85 |
|
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 } |
|
105 |
|
106 if (!paymentRequests.length) { |
|
107 this.paymentFailed(requestId, |
|
108 "PAY_REQUEST_ERROR_NO_VALID_REQUEST_FOUND"); |
|
109 return; |
|
110 } |
|
111 |
|
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 } |
|
126 |
|
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 } |
|
140 |
|
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 } |
|
157 |
|
158 this.showPaymentFlow(aRequestId, selectedProvider, jwt); |
|
159 }; |
|
160 |
|
161 let confirmPaymentErrorCb = this.paymentFailed; |
|
162 |
|
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 }, |
|
181 |
|
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(""); |
|
189 |
|
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 } |
|
200 |
|
201 // Now register the payment providers. |
|
202 for (let i in nums) { |
|
203 let branch = prefService |
|
204 .getBranch(PREF_PAYMENTPROVIDERS_BRANCH + nums[i] + "."); |
|
205 let vals = branch.getChildList(""); |
|
206 if (vals.length == 0) { |
|
207 return; |
|
208 } |
|
209 try { |
|
210 let type = branch.getCharPref("type"); |
|
211 if (type in this.registeredProviders) { |
|
212 continue; |
|
213 } |
|
214 this.registeredProviders[type] = { |
|
215 name: branch.getCharPref("name"), |
|
216 uri: branch.getCharPref("uri"), |
|
217 description: branch.getCharPref("description"), |
|
218 requestMethod: branch.getCharPref("requestMethod") |
|
219 }; |
|
220 if (this._debug) { |
|
221 this.LOG("Registered Payment Providers: " + |
|
222 JSON.stringify(this.registeredProviders[type])); |
|
223 } |
|
224 } catch (ex) { |
|
225 if (this._debug) { |
|
226 this.LOG("An error ocurred registering a payment provider. " + ex); |
|
227 } |
|
228 } |
|
229 } |
|
230 }, |
|
231 |
|
232 /** |
|
233 * Helper for sending a Payment:Failed message to the parent process. |
|
234 */ |
|
235 paymentFailed: function paymentFailed(aRequestId, aErrorMsg) { |
|
236 let mm = this.messageManagers[aRequestId]; |
|
237 mm.sendAsyncMessage("Payment:Failed", { |
|
238 requestId: aRequestId, |
|
239 errorMsg: aErrorMsg |
|
240 }); |
|
241 }, |
|
242 |
|
243 /** |
|
244 * Helper function to get the payment request info according to the jwt |
|
245 * type. Payment provider's data is stored as a preference. |
|
246 */ |
|
247 getPaymentRequestInfo: function getPaymentRequestInfo(aRequestId, aJwt) { |
|
248 if (!aJwt) { |
|
249 this.paymentFailed(aRequestId, "INTERNAL_ERROR_CALL_WITH_MISSING_JWT"); |
|
250 return true; |
|
251 } |
|
252 |
|
253 // First thing, we check that the jwt type is an allowed type and has a |
|
254 // payment provider flow information associated. |
|
255 |
|
256 // A jwt string consists in three parts separated by period ('.'): header, |
|
257 // payload and signature. |
|
258 let segments = aJwt.split('.'); |
|
259 if (segments.length !== 3) { |
|
260 if (this._debug) { |
|
261 this.LOG("Error getting payment provider's uri. " + |
|
262 "Not enough or too many segments"); |
|
263 } |
|
264 this.paymentFailed(aRequestId, |
|
265 "PAY_REQUEST_ERROR_WRONG_SEGMENTS_COUNT"); |
|
266 return true; |
|
267 } |
|
268 |
|
269 let payloadObject; |
|
270 try { |
|
271 // We only care about the payload segment, which contains the jwt type |
|
272 // that should match with any of the stored payment provider's data and |
|
273 // the payment request information to be shown to the user. |
|
274 // Before decoding the JWT string we need to normalize it to be compliant |
|
275 // with RFC 4648. |
|
276 segments[1] = segments[1].replace("-", "+", "g").replace("_", "/", "g"); |
|
277 let payload = atob(segments[1]); |
|
278 if (this._debug) { |
|
279 this.LOG("Payload " + payload); |
|
280 } |
|
281 if (!payload.length) { |
|
282 this.paymentFailed(aRequestId, "PAY_REQUEST_ERROR_EMPTY_PAYLOAD"); |
|
283 return true; |
|
284 } |
|
285 payloadObject = JSON.parse(payload); |
|
286 if (!payloadObject) { |
|
287 this.paymentFailed(aRequestId, |
|
288 "PAY_REQUEST_ERROR_ERROR_PARSING_JWT_PAYLOAD"); |
|
289 return true; |
|
290 } |
|
291 } catch (e) { |
|
292 this.paymentFailed(aRequestId, |
|
293 "PAY_REQUEST_ERROR_ERROR_DECODING_JWT"); |
|
294 return true; |
|
295 } |
|
296 |
|
297 if (!payloadObject.typ) { |
|
298 this.paymentFailed(aRequestId, |
|
299 "PAY_REQUEST_ERROR_NO_TYP_PARAMETER"); |
|
300 return true; |
|
301 } |
|
302 |
|
303 if (!payloadObject.request) { |
|
304 this.paymentFailed(aRequestId, |
|
305 "PAY_REQUEST_ERROR_NO_REQUEST_PARAMETER"); |
|
306 return true; |
|
307 } |
|
308 |
|
309 // Once we got the jwt 'typ' value we look for a match within the payment |
|
310 // providers stored preferences. If the jwt 'typ' is not recognized as one |
|
311 // of the allowed values for registered payment providers, we skip the jwt |
|
312 // validation but we don't fire any error. This way developers might have |
|
313 // a default set of well formed JWTs that might be used in different B2G |
|
314 // devices with a different set of allowed payment providers. |
|
315 let provider = this.registeredProviders[payloadObject.typ]; |
|
316 if (!provider) { |
|
317 if (this._debug) { |
|
318 this.LOG("Not registered payment provider for jwt type: " + |
|
319 payloadObject.typ); |
|
320 } |
|
321 return false; |
|
322 } |
|
323 |
|
324 if (!provider.uri || !provider.name) { |
|
325 this.paymentFailed(aRequestId, |
|
326 "INTERNAL_ERROR_WRONG_REGISTERED_PAY_PROVIDER"); |
|
327 return true; |
|
328 } |
|
329 |
|
330 // We only allow https for payment providers uris. |
|
331 if (this.checkHttps && !/^https/.exec(provider.uri.toLowerCase())) { |
|
332 // We should never get this far. |
|
333 if (this._debug) { |
|
334 this.LOG("Payment provider uris must be https: " + provider.uri); |
|
335 } |
|
336 this.paymentFailed(aRequestId, |
|
337 "INTERNAL_ERROR_NON_HTTPS_PROVIDER_URI"); |
|
338 return true; |
|
339 } |
|
340 |
|
341 let pldRequest = payloadObject.request; |
|
342 return { jwt: aJwt, type: payloadObject.typ, providerName: provider.name }; |
|
343 }, |
|
344 |
|
345 showPaymentFlow: function showPaymentFlow(aRequestId, |
|
346 aPaymentProvider, |
|
347 aJwt) { |
|
348 let paymentFlowInfo = Cc["@mozilla.org/payment/flow-info;1"] |
|
349 .createInstance(Ci.nsIPaymentFlowInfo); |
|
350 paymentFlowInfo.uri = aPaymentProvider.uri; |
|
351 paymentFlowInfo.requestMethod = aPaymentProvider.requestMethod; |
|
352 paymentFlowInfo.jwt = aJwt; |
|
353 |
|
354 let glue = Cc["@mozilla.org/payment/ui-glue;1"] |
|
355 .createInstance(Ci.nsIPaymentUIGlue); |
|
356 if (!glue) { |
|
357 if (this._debug) { |
|
358 this.LOG("Could not create nsIPaymentUIGlue instance"); |
|
359 } |
|
360 this.paymentFailed(aRequestId, |
|
361 "INTERNAL_ERROR_CREATE_PAYMENT_GLUE_FAILED"); |
|
362 return false; |
|
363 } |
|
364 glue.showPaymentFlow(aRequestId, |
|
365 paymentFlowInfo, |
|
366 this.paymentFailed.bind(this)); |
|
367 }, |
|
368 |
|
369 // nsIObserver |
|
370 |
|
371 observe: function observe(subject, topic, data) { |
|
372 if (topic == "xpcom-shutdown") { |
|
373 for each (let msgname in PAYMENT_IPC_MSG_NAMES) { |
|
374 ppmm.removeMessageListener(msgname, this); |
|
375 } |
|
376 this.registeredProviders = null; |
|
377 this.messageManagers = null; |
|
378 |
|
379 Services.obs.removeObserver(this, "xpcom-shutdown"); |
|
380 } |
|
381 }, |
|
382 |
|
383 LOG: function LOG(s) { |
|
384 if (!this._debug) { |
|
385 return; |
|
386 } |
|
387 dump("-*- PaymentManager: " + s + "\n"); |
|
388 } |
|
389 }; |
|
390 |
|
391 PaymentManager.init(); |