michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu} = Components; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: this.EXPORTED_SYMBOLS = []; michael@0: michael@0: const PAYMENT_IPC_MSG_NAMES = ["Payment:Pay", michael@0: "Payment:Success", michael@0: "Payment:Failed"]; michael@0: michael@0: const PREF_PAYMENTPROVIDERS_BRANCH = "dom.payment.provider."; michael@0: const PREF_PAYMENT_BRANCH = "dom.payment."; michael@0: const PREF_DEBUG = "dom.payment.debug"; michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "ppmm", michael@0: "@mozilla.org/parentprocessmessagemanager;1", michael@0: "nsIMessageListenerManager"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "prefService", michael@0: "@mozilla.org/preferences-service;1", michael@0: "nsIPrefService"); michael@0: michael@0: let PaymentManager = { michael@0: init: function init() { michael@0: // Payment providers data are stored as a preference. michael@0: this.registeredProviders = null; michael@0: michael@0: this.messageManagers = {}; michael@0: michael@0: // The dom.payment.skipHTTPSCheck pref is supposed to be used only during michael@0: // development process. This preference should not be active for a michael@0: // production build. michael@0: let paymentPrefs = prefService.getBranch(PREF_PAYMENT_BRANCH); michael@0: this.checkHttps = true; michael@0: try { michael@0: if (paymentPrefs.getPrefType("skipHTTPSCheck")) { michael@0: this.checkHttps = !paymentPrefs.getBoolPref("skipHTTPSCheck"); michael@0: } michael@0: } catch(e) {} michael@0: michael@0: for each (let msgname in PAYMENT_IPC_MSG_NAMES) { michael@0: ppmm.addMessageListener(msgname, this); michael@0: } michael@0: michael@0: Services.obs.addObserver(this, "xpcom-shutdown", false); michael@0: michael@0: try { michael@0: this._debug = michael@0: Services.prefs.getPrefType(PREF_DEBUG) == Ci.nsIPrefBranch.PREF_BOOL michael@0: && Services.prefs.getBoolPref(PREF_DEBUG); michael@0: } catch(e) { michael@0: this._debug = false; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Process a message from the content process. michael@0: */ michael@0: receiveMessage: function receiveMessage(aMessage) { michael@0: let name = aMessage.name; michael@0: let msg = aMessage.json; michael@0: if (this._debug) { michael@0: this.LOG("Received '" + name + "' message from content process"); michael@0: } michael@0: michael@0: switch (name) { michael@0: case "Payment:Pay": { michael@0: // First of all, we register the payment providers. michael@0: if (!this.registeredProviders) { michael@0: this.registeredProviders = {}; michael@0: this.registerPaymentProviders(); michael@0: } michael@0: michael@0: // We save the message target message manager so we can later dispatch michael@0: // back messages without broadcasting to all child processes. michael@0: let requestId = msg.requestId; michael@0: this.messageManagers[requestId] = aMessage.target; michael@0: michael@0: // We check the jwt type and look for a match within the michael@0: // registered payment providers to get the correct payment request michael@0: // information. michael@0: let paymentRequests = []; michael@0: let jwtTypes = []; michael@0: for (let i in msg.jwts) { michael@0: let pr = this.getPaymentRequestInfo(requestId, msg.jwts[i]); michael@0: if (!pr) { michael@0: continue; michael@0: } michael@0: // We consider jwt type repetition an error. michael@0: if (jwtTypes[pr.type]) { michael@0: this.paymentFailed(requestId, michael@0: "PAY_REQUEST_ERROR_DUPLICATED_JWT_TYPE"); michael@0: return; michael@0: } michael@0: jwtTypes[pr.type] = true; michael@0: paymentRequests.push(pr); michael@0: } michael@0: michael@0: if (!paymentRequests.length) { michael@0: this.paymentFailed(requestId, michael@0: "PAY_REQUEST_ERROR_NO_VALID_REQUEST_FOUND"); michael@0: return; michael@0: } michael@0: michael@0: // After getting the list of valid payment requests, we ask the user michael@0: // for confirmation before sending any request to any payment provider. michael@0: // If there is more than one choice, we also let the user select the one michael@0: // that he prefers. michael@0: let glue = Cc["@mozilla.org/payment/ui-glue;1"] michael@0: .createInstance(Ci.nsIPaymentUIGlue); michael@0: if (!glue) { michael@0: if (this._debug) { michael@0: this.LOG("Could not create nsIPaymentUIGlue instance"); michael@0: } michael@0: this.paymentFailed(requestId, michael@0: "INTERNAL_ERROR_CREATE_PAYMENT_GLUE_FAILED"); michael@0: return; michael@0: } michael@0: michael@0: let confirmPaymentSuccessCb = function successCb(aRequestId, michael@0: aResult) { michael@0: // Get the appropriate payment provider data based on user's choice. michael@0: let selectedProvider = this.registeredProviders[aResult]; michael@0: if (!selectedProvider || !selectedProvider.uri) { michael@0: if (this._debug) { michael@0: this.LOG("Could not retrieve a valid provider based on user's " + michael@0: "selection"); michael@0: } michael@0: this.paymentFailed(aRequestId, michael@0: "INTERNAL_ERROR_NO_VALID_SELECTED_PROVIDER"); michael@0: return; michael@0: } michael@0: michael@0: let jwt; michael@0: for (let i in paymentRequests) { michael@0: if (paymentRequests[i].type == aResult) { michael@0: jwt = paymentRequests[i].jwt; michael@0: break; michael@0: } michael@0: } michael@0: if (!jwt) { michael@0: if (this._debug) { michael@0: this.LOG("The selected request has no JWT information " + michael@0: "associated"); michael@0: } michael@0: this.paymentFailed(aRequestId, michael@0: "INTERNAL_ERROR_NO_JWT_ASSOCIATED_TO_REQUEST"); michael@0: return; michael@0: } michael@0: michael@0: this.showPaymentFlow(aRequestId, selectedProvider, jwt); michael@0: }; michael@0: michael@0: let confirmPaymentErrorCb = this.paymentFailed; michael@0: michael@0: glue.confirmPaymentRequest(requestId, michael@0: paymentRequests, michael@0: confirmPaymentSuccessCb.bind(this), michael@0: confirmPaymentErrorCb.bind(this)); michael@0: break; michael@0: } michael@0: case "Payment:Success": michael@0: case "Payment:Failed": { michael@0: let mm = this.messageManagers[msg.requestId]; michael@0: mm.sendAsyncMessage(name, { michael@0: requestId: msg.requestId, michael@0: result: msg.result, michael@0: errorMsg: msg.errorMsg michael@0: }); michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Helper function to register payment providers stored as preferences. michael@0: */ michael@0: registerPaymentProviders: function registerPaymentProviders() { michael@0: let paymentProviders = prefService michael@0: .getBranch(PREF_PAYMENTPROVIDERS_BRANCH) michael@0: .getChildList(""); michael@0: michael@0: // First get the numbers of the providers by getting all ###.uri prefs. michael@0: let nums = []; michael@0: for (let i in paymentProviders) { michael@0: let match = /^(\d+)\.uri$/.exec(paymentProviders[i]); michael@0: if (!match) { michael@0: continue; michael@0: } else { michael@0: nums.push(match[1]); michael@0: } michael@0: } michael@0: michael@0: // Now register the payment providers. michael@0: for (let i in nums) { michael@0: let branch = prefService michael@0: .getBranch(PREF_PAYMENTPROVIDERS_BRANCH + nums[i] + "."); michael@0: let vals = branch.getChildList(""); michael@0: if (vals.length == 0) { michael@0: return; michael@0: } michael@0: try { michael@0: let type = branch.getCharPref("type"); michael@0: if (type in this.registeredProviders) { michael@0: continue; michael@0: } michael@0: this.registeredProviders[type] = { michael@0: name: branch.getCharPref("name"), michael@0: uri: branch.getCharPref("uri"), michael@0: description: branch.getCharPref("description"), michael@0: requestMethod: branch.getCharPref("requestMethod") michael@0: }; michael@0: if (this._debug) { michael@0: this.LOG("Registered Payment Providers: " + michael@0: JSON.stringify(this.registeredProviders[type])); michael@0: } michael@0: } catch (ex) { michael@0: if (this._debug) { michael@0: this.LOG("An error ocurred registering a payment provider. " + ex); michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Helper for sending a Payment:Failed message to the parent process. michael@0: */ michael@0: paymentFailed: function paymentFailed(aRequestId, aErrorMsg) { michael@0: let mm = this.messageManagers[aRequestId]; michael@0: mm.sendAsyncMessage("Payment:Failed", { michael@0: requestId: aRequestId, michael@0: errorMsg: aErrorMsg michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Helper function to get the payment request info according to the jwt michael@0: * type. Payment provider's data is stored as a preference. michael@0: */ michael@0: getPaymentRequestInfo: function getPaymentRequestInfo(aRequestId, aJwt) { michael@0: if (!aJwt) { michael@0: this.paymentFailed(aRequestId, "INTERNAL_ERROR_CALL_WITH_MISSING_JWT"); michael@0: return true; michael@0: } michael@0: michael@0: // First thing, we check that the jwt type is an allowed type and has a michael@0: // payment provider flow information associated. michael@0: michael@0: // A jwt string consists in three parts separated by period ('.'): header, michael@0: // payload and signature. michael@0: let segments = aJwt.split('.'); michael@0: if (segments.length !== 3) { michael@0: if (this._debug) { michael@0: this.LOG("Error getting payment provider's uri. " + michael@0: "Not enough or too many segments"); michael@0: } michael@0: this.paymentFailed(aRequestId, michael@0: "PAY_REQUEST_ERROR_WRONG_SEGMENTS_COUNT"); michael@0: return true; michael@0: } michael@0: michael@0: let payloadObject; michael@0: try { michael@0: // We only care about the payload segment, which contains the jwt type michael@0: // that should match with any of the stored payment provider's data and michael@0: // the payment request information to be shown to the user. michael@0: // Before decoding the JWT string we need to normalize it to be compliant michael@0: // with RFC 4648. michael@0: segments[1] = segments[1].replace("-", "+", "g").replace("_", "/", "g"); michael@0: let payload = atob(segments[1]); michael@0: if (this._debug) { michael@0: this.LOG("Payload " + payload); michael@0: } michael@0: if (!payload.length) { michael@0: this.paymentFailed(aRequestId, "PAY_REQUEST_ERROR_EMPTY_PAYLOAD"); michael@0: return true; michael@0: } michael@0: payloadObject = JSON.parse(payload); michael@0: if (!payloadObject) { michael@0: this.paymentFailed(aRequestId, michael@0: "PAY_REQUEST_ERROR_ERROR_PARSING_JWT_PAYLOAD"); michael@0: return true; michael@0: } michael@0: } catch (e) { michael@0: this.paymentFailed(aRequestId, michael@0: "PAY_REQUEST_ERROR_ERROR_DECODING_JWT"); michael@0: return true; michael@0: } michael@0: michael@0: if (!payloadObject.typ) { michael@0: this.paymentFailed(aRequestId, michael@0: "PAY_REQUEST_ERROR_NO_TYP_PARAMETER"); michael@0: return true; michael@0: } michael@0: michael@0: if (!payloadObject.request) { michael@0: this.paymentFailed(aRequestId, michael@0: "PAY_REQUEST_ERROR_NO_REQUEST_PARAMETER"); michael@0: return true; michael@0: } michael@0: michael@0: // Once we got the jwt 'typ' value we look for a match within the payment michael@0: // providers stored preferences. If the jwt 'typ' is not recognized as one michael@0: // of the allowed values for registered payment providers, we skip the jwt michael@0: // validation but we don't fire any error. This way developers might have michael@0: // a default set of well formed JWTs that might be used in different B2G michael@0: // devices with a different set of allowed payment providers. michael@0: let provider = this.registeredProviders[payloadObject.typ]; michael@0: if (!provider) { michael@0: if (this._debug) { michael@0: this.LOG("Not registered payment provider for jwt type: " + michael@0: payloadObject.typ); michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: if (!provider.uri || !provider.name) { michael@0: this.paymentFailed(aRequestId, michael@0: "INTERNAL_ERROR_WRONG_REGISTERED_PAY_PROVIDER"); michael@0: return true; michael@0: } michael@0: michael@0: // We only allow https for payment providers uris. michael@0: if (this.checkHttps && !/^https/.exec(provider.uri.toLowerCase())) { michael@0: // We should never get this far. michael@0: if (this._debug) { michael@0: this.LOG("Payment provider uris must be https: " + provider.uri); michael@0: } michael@0: this.paymentFailed(aRequestId, michael@0: "INTERNAL_ERROR_NON_HTTPS_PROVIDER_URI"); michael@0: return true; michael@0: } michael@0: michael@0: let pldRequest = payloadObject.request; michael@0: return { jwt: aJwt, type: payloadObject.typ, providerName: provider.name }; michael@0: }, michael@0: michael@0: showPaymentFlow: function showPaymentFlow(aRequestId, michael@0: aPaymentProvider, michael@0: aJwt) { michael@0: let paymentFlowInfo = Cc["@mozilla.org/payment/flow-info;1"] michael@0: .createInstance(Ci.nsIPaymentFlowInfo); michael@0: paymentFlowInfo.uri = aPaymentProvider.uri; michael@0: paymentFlowInfo.requestMethod = aPaymentProvider.requestMethod; michael@0: paymentFlowInfo.jwt = aJwt; michael@0: michael@0: let glue = Cc["@mozilla.org/payment/ui-glue;1"] michael@0: .createInstance(Ci.nsIPaymentUIGlue); michael@0: if (!glue) { michael@0: if (this._debug) { michael@0: this.LOG("Could not create nsIPaymentUIGlue instance"); michael@0: } michael@0: this.paymentFailed(aRequestId, michael@0: "INTERNAL_ERROR_CREATE_PAYMENT_GLUE_FAILED"); michael@0: return false; michael@0: } michael@0: glue.showPaymentFlow(aRequestId, michael@0: paymentFlowInfo, michael@0: this.paymentFailed.bind(this)); michael@0: }, michael@0: michael@0: // nsIObserver michael@0: michael@0: observe: function observe(subject, topic, data) { michael@0: if (topic == "xpcom-shutdown") { michael@0: for each (let msgname in PAYMENT_IPC_MSG_NAMES) { michael@0: ppmm.removeMessageListener(msgname, this); michael@0: } michael@0: this.registeredProviders = null; michael@0: this.messageManagers = null; michael@0: michael@0: Services.obs.removeObserver(this, "xpcom-shutdown"); michael@0: } michael@0: }, michael@0: michael@0: LOG: function LOG(s) { michael@0: if (!this._debug) { michael@0: return; michael@0: } michael@0: dump("-*- PaymentManager: " + s + "\n"); michael@0: } michael@0: }; michael@0: michael@0: PaymentManager.init();