lib/adyen/form.rb in adyen-1.3.1 vs lib/adyen/form.rb in adyen-1.3.2

- old
+ new

@@ -1,20 +1,20 @@ require 'cgi' module Adyen - # The Adyen::Form module contains all functionality that is used to send payment requests - # to the Adyen payment system, using either a HTML form (see {Adyen::Form.hidden_fields}) + # The Adyen::Form module contains all functionality that is used to send payment requests + # to the Adyen payment system, using either a HTML form (see {Adyen::Form.hidden_fields}) # or a HTTP redirect (see {Adyen::Form.redirect_url}). # # Moreover, this module contains the method {Adyen::Form.redirect_signature_check} to # check the request, that is made to your website after the visitor has made his payment # on the Adyen system, for genuinity. # # You can use different skins in Adyen to define different payment environments. You can # register these skins under a custom name in the module. The other methods will automatically - # use this information (i.e. the skin code and the shared secret) if it is available. + # use this information (i.e. the skin code and the shared secret) if it is available. # Otherwise, you have to provide it yourself for every method call you make. See # {Adyen::Configuration#register_form_skin} for more information. # # @see Adyen::Configuration#register_form_skin # @see Adyen::Form.hidden_fields @@ -25,26 +25,45 @@ ###################################################### # ADYEN FORM URL ###################################################### + # The DOMAIN of the Adyen payment system that still requires the current + # Adyen enviroment. + ACTION_DOMAIN = "%s.adyen.com" + # The URL of the Adyen payment system that still requires the current - # Adyen enviroment and payment flow to be filled. - ACTION_URL = "https://%s.adyen.com/hpp/%s.shtml" + # domain and payment flow to be filled. + ACTION_URL = "https://%s/hpp/%s.shtml" + # Returns the DOMAIN of the Adyen payment system, adjusted for an Adyen environment. + # + # @param [String] environment The Adyen environment to use. This parameter can be + # left out, in which case the 'current' environment will be used. + # @return [String] The domain of the Adyen payment system that can be used + # for payment forms or redirects. + # @see Adyen::Form.environment + # @see Adyen::Form.redirect_url + def domain(environment = nil) + environment ||= Adyen.configuration.environment + (Adyen.configuration.payment_flow_domain || ACTION_DOMAIN) % [environment.to_s] + end + # Returns the URL of the Adyen payment system, adjusted for an Adyen environment. # - # @param [String] environment The Adyen environment to use. This parameter can be + # @param [String] environment The Adyen environment to use. This parameter can be # left out, in which case the 'current' environment will be used. + # @param [String] payment_flow The Adyen payment type to use. This parameter can be + # left out, in which case the default payment type will be used. # @return [String] The absolute URL of the Adyen payment system that can be used # for payment forms or redirects. # @see Adyen::Form.environment + # @see Adyen::Form.domain # @see Adyen::Form.redirect_url def url(environment = nil, payment_flow = nil) - environment ||= Adyen.configuration.environment payment_flow ||= Adyen.configuration.payment_flow - Adyen::Form::ACTION_URL % [environment.to_s, payment_flow.to_s] + Adyen::Form::ACTION_URL % [domain(environment), payment_flow.to_s] end ###################################################### # POSTING/REDIRECTING TO ADYEN ###################################################### @@ -56,86 +75,102 @@ # # @private # @param [Hash] parameters The payment parameters hash to transform def do_parameter_transformations!(parameters = {}) parameters.replace(Adyen.configuration.default_form_params.merge(parameters)) - parameters[:recurring_contract] = 'RECURRING' if parameters.delete(:recurring) == true - parameters[:order_data] = Adyen::Encoding.gzip_base64(parameters.delete(:order_data_raw)) if parameters[:order_data_raw] - parameters[:ship_before_date] = Adyen::Formatter::DateTime.fmt_date(parameters[:ship_before_date]) - parameters[:session_validity] = Adyen::Formatter::DateTime.fmt_time(parameters[:session_validity]) - + if parameters[:skin] skin = Adyen.configuration.form_skin_by_name(parameters.delete(:skin)) parameters[:skin_code] ||= skin[:skin_code] parameters[:shared_secret] ||= skin[:shared_secret] + parameters.merge!(skin[:default_form_params]) end + + parameters[:recurring_contract] = 'RECURRING' if parameters.delete(:recurring) == true + parameters[:order_data] = Adyen::Encoding.gzip_base64(parameters.delete(:order_data_raw)) if parameters[:order_data_raw] + parameters[:ship_before_date] = Adyen::Formatter::DateTime.fmt_date(parameters[:ship_before_date]) + parameters[:session_validity] = Adyen::Formatter::DateTime.fmt_time(parameters[:session_validity]) end # Transforms the payment parameters to be in the correct format and calculates the merchant # signature parameter. It also does some basic health checks on the parameters hash. # - # @param [Hash] parameters The payment parameters. The parameters set in the + # @param [Hash] parameters The payment parameters. The parameters set in the # {Adyen::Configuration#default_form_params} hash will be included automatically. # @param [String] shared_secret The shared secret that should be used to calculate # the payment request signature. This parameter can be left if the skin that is # used is registered (see {Adyen::Configuration#register_form_skin}), or if the # shared secret is provided as the +:shared_secret+ parameter. # @return [Hash] The payment parameters with the +:merchant_signature+ parameter set. # @raise [ArgumentError] Thrown if some parameter health check fails. def payment_parameters(parameters = {}, shared_secret = nil) do_parameter_transformations!(parameters) - + raise ArgumentError, "Cannot generate form: :currency code attribute not found!" unless parameters[:currency_code] raise ArgumentError, "Cannot generate form: :payment_amount code attribute not found!" unless parameters[:payment_amount] raise ArgumentError, "Cannot generate form: :merchant_account attribute not found!" unless parameters[:merchant_account] raise ArgumentError, "Cannot generate form: :skin_code attribute not found!" unless parameters[:skin_code] # Calculate the merchant signature using the shared secret. shared_secret ||= parameters.delete(:shared_secret) raise ArgumentError, "Cannot calculate payment request signature without shared secret!" unless shared_secret parameters[:merchant_sig] = calculate_signature(parameters, shared_secret) - + + if parameters[:billing_address] + parameters[:billing_address_sig] = calculate_billing_address_signature(parameters, shared_secret) + end + return parameters end - + + # Transforms and flattens payment parameters to be in the correct format which is understood and accepted by adyen + # + # @param [Hash] parameters The payment parameters. The parameters set in the + # {Adyen::Configuration#default_form_params} hash will be included automatically. + # @return [Hash] The payment parameters flatten, with camelized and prefixed key, stringified value + def flat_payment_parameters(parameters = {}) + flatten(payment_parameters(parameters)) + end + # Returns an absolute URL to the Adyen payment system, with the payment parameters included # as GET parameters in the URL. The URL also depends on the current Adyen enviroment. # - # The payment parameters that are provided to this method will be merged with the + # The payment parameters that are provided to this method will be merged with the # {Adyen::Configuration#default_form_params} hash. The default parameter values will be # overrided if another value is provided to this method. # # You do not have to provide the +:merchant_sig+ parameter: it will be calculated automatically # if you provide either a registered skin name as the +:skin+ parameter or provide both the # +:skin_code+ and +:shared_secret+ parameters. # - # Note that Internet Explorer has a maximum length for URLs it can handle (2083 characters). + # Note that Internet Explorer has a maximum length for URLs it can handle (2083 characters). # Make sure that the URL is not longer than this limit if you want your site to work in IE. # # @example # # def pay # # Genarate a URL to redirect to Adyen's payment system. # adyen_url = Adyen::Form.redirect_url(:skin => :my_skin, :currency_code => 'USD', # :payment_amount => 1000, merchant_account => 'MyMerchant', ... ) - # + # # respond_to do |format| # format.html { redirect_to(adyen_url) } # end # end # # @param [Hash] parameters The payment parameters to include in the payment request. # @return [String] An absolute URL to redirect to the Adyen payment system. def redirect_url(parameters = {}) - url + '?' + payment_parameters(parameters).map{|k,v| [k.to_s,v] }.sort.map { |(k, v)| - "#{camelize(k)}=#{CGI.escape(v.to_s)}" }.join('&') + url + '?' + flat_payment_parameters(parameters).map { |(k, v)| + "#{k}=#{CGI.escape(v)}" + }.join('&') end - - # Returns a HTML snippet of hidden INPUT tags with the provided payment parameters. + + # Returns a HTML snippet of hidden INPUT tags with the provided payment parameters. # The snippet can be included in a payment form that POSTs to the Adyen payment system. # - # The payment parameters that are provided to this method will be merged with the + # The payment parameters that are provided to this method will be merged with the # {Adyen::Configuration#default_form_params} hash. The default parameter values will be # overrided if another value is provided to this method. # # You do not have to provide the +:merchant_sig+ parameter: it will be calculated automatically # if you provide either a registered skin name as the +:skin+ parameter or provide both the @@ -150,19 +185,19 @@ # # @param [Hash] parameters The payment parameters to include in the payment request. # @return [String] An HTML snippet that can be included in a form that POSTs to the # Adyen payment system. def hidden_fields(parameters = {}) - + # Generate a hidden input tag per parameter, join them by newlines. - form_str = payment_parameters(parameters).map { |key, value| - "<input type=\"hidden\" name=\"#{CGI.escapeHTML(camelize(key))}\" value=\"#{CGI.escapeHTML(value.to_s)}\" />" + form_str = flat_payment_parameters(parameters).map { |key, value| + "<input type=\"hidden\" name=\"#{CGI.escapeHTML(key)}\" value=\"#{CGI.escapeHTML(value)}\" />" }.join("\n") - + form_str.respond_to?(:html_safe) ? form_str.html_safe : form_str end - + ###################################################### # MERCHANT SIGNATURE CALCULATION ###################################################### # Generates the string that is used to calculate the request signature. This signature @@ -179,50 +214,84 @@ parameters[:allowed_methods].to_s << parameters[:blocked_methods].to_s << parameters[:shopper_statement].to_s << parameters[:merchant_return_data].to_s << parameters[:billing_address_type].to_s << parameters[:offset].to_s end - # Calculates the payment request signature for the given payment parameters. + # Calculates the payment request signature for the given payment parameters. # # This signature is used by Adyen to check whether the request is # genuinely originating from you. The resulting signature should be # included in the payment request parameters as the +merchantSig+ # parameter; the shared secret should of course not be included. # # @param [Hash] parameters The payment parameters for which to calculate # the payment request signature. - # @param [String] shared_secret The shared secret to use for this signature. - # It should correspond with the skin_code parameter. This parameter can be + # @param [String] shared_secret The shared secret to use for this signature. + # It should correspond with the skin_code parameter. This parameter can be # left out if the shared_secret is included as key in the parameters. # @return [String] The signature of the payment request + # @raise [ArgumentError] Thrown if shared_secret is empty def calculate_signature(parameters, shared_secret = nil) shared_secret ||= parameters.delete(:shared_secret) + raise ArgumentError, "Cannot calculate payment request signature with empty shared_secret" if shared_secret.to_s.empty? Adyen::Encoding.hmac_base64(shared_secret, calculate_signature_string(parameters)) end + # Generates the string that is used to calculate the request signature. This signature + # is used by Adyen to check whether the request is genuinely originating from you. + # @param [Hash] parameters The parameters that will be included in the billing address request. + # @return [String] The string for which the siganture is calculated. + def calculate_billing_address_signature_string(parameters) + %w(street house_number_or_name city postal_code state_or_province country).map do |key| + parameters[key.to_sym] + end.join + end + + # Calculates the billing address request signature for the given billing address parameters. + # + # This signature is used by Adyen to check whether the request is + # genuinely originating from you. The resulting signature should be + # included in the billing address request parameters as the +billingAddressSig+ + # parameter; the shared secret should of course not be included. + # + # @param [Hash] parameters The billing address parameters for which to calculate + # the billing address request signature. + # @param [String] shared_secret The shared secret to use for this signature. + # It should correspond with the skin_code parameter. This parameter can be + # left out if the shared_secret is included as key in the parameters. + # @return [String] The signature of the billing address request + # @raise [ArgumentError] Thrown if shared_secret is empty + def calculate_billing_address_signature(parameters, shared_secret = nil) + shared_secret ||= parameters.delete(:shared_secret) + raise ArgumentError, "Cannot calculate billing address request signature with empty shared_secret" if shared_secret.to_s.empty? + Adyen::Encoding.hmac_base64(shared_secret, calculate_billing_address_signature_string(parameters[:billing_address])) + end + ###################################################### # REDIRECT SIGNATURE CHECKING ###################################################### # Generates the string for which the redirect signature is calculated, using the request paramaters. # @param [Hash] params A hash of HTTP GET parameters for the redirect request. # @return [String] The signature string. def redirect_signature_string(params) - params[:authResult].to_s + params[:pspReference].to_s + params[:merchantReference].to_s + + params[:authResult].to_s + params[:pspReference].to_s + params[:merchantReference].to_s + params[:skinCode].to_s + params[:merchantReturnData].to_s end - - # Computes the redirect signature using the request parameters, so that the + + # Computes the redirect signature using the request parameters, so that the # redirect can be checked for forgery. # # @param [Hash] params A hash of HTTP GET parameters for the redirect request. # @param [String] shared_secret The shared secret for the Adyen skin that was used for - # the original payment form. You can leave this out of the skin is registered + # the original payment form. You can leave this out of the skin is registered # using the {Adyen::Form.register_skin} method. # @return [String] The redirect signature + # @raise [ArgumentError] Thrown if shared_secret is empty def redirect_signature(params, shared_secret = nil) shared_secret ||= Adyen.configuration.form_skin_shared_secret_by_code(params[:skinCode]) + raise ArgumentError, "Cannot compute redirect signature with empty shared_secret" if shared_secret.to_s.empty? Adyen::Encoding.hmac_base64(shared_secret, redirect_signature_string(params)) end # Checks the redirect signature for this request by calcultating the signature from # the provided parameters, and comparing it to the signature provided in the +merchantSig+ @@ -233,37 +302,64 @@ # signature check fails. # # @example # class PaymentsController < ApplicationController # before_filter :check_signature, :only => [:return_from_adyen] - # + # # def return_from_adyen # @invoice = Invoice.find(params[:merchantReference]) # @invoice.set_paid! if params[:authResult] == 'AUTHORISED' # end - # + # # private - # + # # def check_signature # raise "Forgery!" unless Adyen::Form.redirect_signature_check(params) # end # end # # @param [Hash] params params A hash of HTTP GET parameters for the redirect request. This # should include the +:merchantSig+ parameter, which contains the signature. # @param [String] shared_secret The shared secret for the Adyen skin that was used for - # the original payment form. You can leave this out of the skin is registered + # the original payment form. You can leave this out of the skin is registered # using the {Adyen::Configuration#register_form_skin} method. # @return [true, false] Returns true only if the signature in the parameters is correct. def redirect_signature_check(params, shared_secret = nil) params[:merchantSig] == redirect_signature(params, shared_secret) end - + # Returns the camelized version of a string. # @param [:to_s] identifier The identifier to turn to camelcase # @return [String] The camelcase version of the identifier provided. def camelize(identifier) identifier.to_s.gsub(/_(.)/) { $1.upcase } end - + + # Transforms the nested parameters Hash into a 'flat' Hash which is understood by adyen. This is: + # * all keys are camelized + # * all keys are stringified + # * nested hash is flattened, keys are prefixed with root key + # + # @example + # flatten {:billing_address => { :street => 'My Street'}} + # + # # resolves in: + # {'billingAddress.street' => 'My Street'} + # + # @param [Hash] parameters The payment parameters which to transform + # @param [String] prefix The prefix to add to the key + # @param [Hash] return_hash The new hash which is retruned (needed for recursive calls) + # @return [Hash] The return_hash filled with camelized and prefixed key, stringified value + def flatten(parameters, prefix = "", return_hash = {}) + parameters ||= {} + parameters.inject(return_hash) do |hash, (key, value)| + key = "#{prefix}#{camelize(key)}" + if value.is_a?(Hash) + flatten(value, "#{key}.", return_hash) + else + hash[key] = value.to_s + end + hash + end + end end end