lib/adyen/form.rb in adyen-0.2.3 vs lib/adyen/form.rb in adyen-0.3.0

- old
+ new

@@ -1,71 +1,140 @@ require 'action_view' 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}) + # or a HTTP redirect (see {Adyen::Form.redirect_url}). + # + # Moreover, this module contains a 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. + # Otherwise, you have to provide it yourself for every method call you make. See + # {Adyen::Form.register_skin} for more information. + # + # @see Adyen::Form.register_skin + # @see Adyen::Form.hidden_fields + # @see Adyen::Form.redirect_url + # @see Adyen::Form.redirect_signature_check module Form extend ActionView::Helpers::TagHelper ###################################################### # SKINS ###################################################### + # Returns all registered skins and their accompanying skin code and shared secret. + # @return [Hash] The hash of registered skins. def self.skins @skins ||= {} end + # Sets the registered skins. + # @param [Hash<Symbol, Hash>] hash A hash with the skin name as key and the skin parameter hash + # (which should include +:skin_code+ and +:shared_secret+) as value. + # @see Adyen::Form.register_skin def self.skins=(hash) @skins = hash.inject({}) do |skins, (name, skin)| skins[name.to_sym] = skin.merge(:name => name.to_sym) skins end end - + + # Registers a skin for later use. + # + # You can store a skin using a self defined symbol. Once the skin is registered, + # you can refer to it using this symbol instead of the hard-to-remember skin code. + # Moreover, the skin's shared_secret will be looked up automatically for calculting + # signatures. + # + # @example + # Adyen::Form.register_skin(:my_skin, 'dsfH67PO', 'Dfs*7uUln9') + # @param [Symbol] name The name of the skin. + # @param [String] skin_code The skin code for this skin, as defined by Adyen. + # @param [String] shared_secret The shared secret used for signature calculation. + # @see Adyen.load_config def self.register_skin(name, skin_code, shared_secret) self.skins[name.to_sym] = {:name => name.to_sym, :skin_code => skin_code, :shared_secret => shared_secret } end + # Returns skin information given a skin name. + # @param [Symbol] skin_name The name of the skin + # @return [Hash, nil] A hash with the skin information, or nil if not found. def self.skin_by_name(skin_name) self.skins[skin_name.to_sym] end - + + # Returns skin information given a skin code. + # @param [String] skin_code The skin code of the skin + # @return [Hash, nil] A hash with the skin information, or nil if not found. def self.skin_by_code(skin_code) - self.skins.detect { |(name, skin)| skin[:skin_code] == skin_code }.last rescue nil + self.skins.detect { |(name, skin)| skin[:skin_code] == skin_code }.last rescue nil end - + + # Returns the shared secret belonging to a skin code. + # @param [String] skin_code The skin code of the skin + # @return [String, nil] The shared secret for the skin, or nil if not found. def self.lookup_shared_secret(skin_code) skin = skin_by_code(skin_code)[:shared_secret] rescue nil - end - + end + ###################################################### # DEFAULT FORM / REDIRECT PARAMETERS - ###################################################### + ###################################################### + # Returns the default parameters to use, unless they are overridden. + # @see Adyen::Form.default_parameters + # @return [Hash] The hash of default parameters def self.default_parameters @default_arguments ||= {} end + # Sets the default parameters to use. + # @see Adyen::Form.default_parameters + # @param [Hash] hash The hash of default parameters def self.default_parameters=(hash) @default_arguments = hash end ###################################################### # ADYEN FORM URL ###################################################### + # The URL of the Adyen payment system that still requires the current + # Adyen enviroment to be filled in. ACTION_URL = "https://%s.adyen.com/hpp/select.shtml" + # 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 + # left out, in which case the 'current' environment 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.redirect_url def self.url(environment = nil) - environment ||= Adyen.environment(environment) + environment ||= Adyen.environment Adyen::Form::ACTION_URL % environment.to_s end - ###################################################### # POSTING/REDIRECTING TO ADYEN ###################################################### + # Transforms the payment parameters hash to be in the correct format. + # It will also include the default_parameters hash. Finally, switches + # the +:skin+ parameter out for the +:skin_code+ and +:shared_secret+ + # parameter using the list of registered skins. + # + # @private + # @param [Hash] parameters The payment parameters hash to transform def self.do_parameter_transformations!(parameters = {}) raise "YENs are not yet supported!" if parameters[:currency_code] == 'JPY' # TODO: fixme parameters.replace(default_parameters.merge(parameters)) parameters[:recurring_contract] = 'DEFAULT' if parameters.delete(:recurring) == true @@ -78,39 +147,107 @@ parameters[:skin_code] ||= skin[:skin_code] parameters[:shared_secret] ||= skin[:shared_secret] end end - def self.payment_parameters(parameters = {}) + # 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 + # {Adyen::Form.default_parameters} 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::Form.register_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 [StandardError] Thrown if some parameter health check fails. + def self.payment_parameters(parameters = {}, shared_secret = nil) do_parameter_transformations!(parameters) raise "Cannot generate form: :currency code attribute not found!" unless parameters[:currency_code] raise "Cannot generate form: :payment_amount code attribute not found!" unless parameters[:payment_amount] raise "Cannot generate form: :merchant_account attribute not found!" unless parameters[:merchant_account] raise "Cannot generate form: :skin_code attribute not found!" unless parameters[:skin_code] - raise "Cannot generate form: :shared_secret signing secret not provided!" unless parameters[:shared_secret] - # Merchant signature - parameters[:merchant_sig] = calculate_signature(parameters) - return parameters + # Calculate the merchant signature using the shared secret. + shared_secret ||= parameters.delete(:shared_secret) + raise "Cannot calculate payment request signature without shared secret!" unless shared_secret + parameters[:merchant_sig] = calculate_signature(parameters, shared_secret) + + return 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 + # {Adyen::Form.default_parameters} 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). + # 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 self.redirect_url(parameters = {}) - self.url + '?' + payment_parameters(parameters).map { |(k, v)| "#{k.to_s.camelize(:lower)}=#{CGI.escape(v.to_s)}" }.join('&') + self.url + '?' + payment_parameters(parameters).map { |(k, v)| + "#{k.to_s.camelize(:lower)}=#{CGI.escape(v.to_s)}" }.join('&') end - + + # 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 + # {Adyen::Form.default_parameters} 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. + # + # @example + # <% form_tag(Adyen::Form.url) do %> + # <%= Adyen::Form.hidden_fields(:skin => :my_skin, :currency_code => 'USD', + # :payment_amount => 1000, ...) %> + # <%= submit_tag("Pay invoice") + # <% end %> + # + # @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 self.hidden_fields(parameters = {}) - # Generate hidden input tags + + # Generate a hidden input tag per parameter, join them by newlines. payment_parameters(parameters).map { |key, value| self.tag(:input, :type => 'hidden', :name => key.to_s.camelize(:lower), :value => value) }.join("\n") end ###################################################### # MERCHANT SIGNATURE CALCULATION ###################################################### + # 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 payment request. + # @return [String] The string for which the siganture is calculated. def self.calculate_signature_string(parameters) merchant_sig_string = "" merchant_sig_string << parameters[:payment_amount].to_s << parameters[:currency_code].to_s << parameters[:ship_before_date].to_s << parameters[:merchant_reference].to_s << parameters[:skin_code].to_s << parameters[:merchant_account].to_s << @@ -118,28 +255,82 @@ parameters[:shopper_reference].to_s << parameters[:recurring_contract].to_s << parameters[:allowed_methods].to_s << parameters[:blocked_methods].to_s << parameters[:shopper_statement].to_s << parameters[:billing_address_type].to_s end - def self.calculate_signature(parameters) - Adyen::Encoding.hmac_base64(parameters.delete(:shared_secret), calculate_signature_string(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 + # left out if the shared_secret is included as key in the parameters. + # @return [String] The signature of the payment request + def self.calculate_signature(parameters, shared_secret = nil) + shared_secret ||= parameters.delete(:shared_secret) + Adyen::Encoding.hmac_base64(shared_secret, calculate_signature_string(parameters)) 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 self.redirect_signature_string(params) params[:authResult].to_s + params[:pspReference].to_s + params[:merchantReference].to_s + params[:skinCode].to_s end - + + # 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 + # using the {Adyen::Form.register_skin} method. + # @return [String] The redirect signature def self.redirect_signature(params, shared_secret = nil) shared_secret ||= lookup_shared_secret(params[:skinCode]) 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+ + # parameter. + # + # If this method returns false, the request could be a forgery and should not be handled. + # Therefore, you should include this check in a +before_filter+, and raise an error of the + # 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 + # using the {Adyen::Form.register_skin} method. + # @return [true, false] Returns true only if the signature in the parameters is correct. def self.redirect_signature_check(params, shared_secret = nil) params[:merchantSig] == redirect_signature(params, shared_secret) end - end -end \ No newline at end of file +end