fingertips-adyen 0.3.7.20100917

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,21 @@
1
+ require 'base64'
2
+ require 'openssl'
3
+ require 'stringio'
4
+ require 'zlib'
5
+
6
+ module Adyen
7
+ module Encoding
8
+ def self.hmac_base64(hmac_key, message)
9
+ digest = OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha1'), hmac_key, message)
10
+ Base64.encode64(digest).strip
11
+ end
12
+
13
+ def self.gzip_base64(message)
14
+ sio = StringIO.new
15
+ gz = Zlib::GzipWriter.new(sio)
16
+ gz.write(message)
17
+ gz.close
18
+ Base64.encode64(sio.string).gsub("\n", "")
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,336 @@
1
+ require 'action_view'
2
+
3
+ module Adyen
4
+
5
+ # The Adyen::Form module contains all functionality that is used to send payment requests
6
+ # to the Adyen payment system, using either a HTML form (see {Adyen::Form.hidden_fields})
7
+ # or a HTTP redirect (see {Adyen::Form.redirect_url}).
8
+ #
9
+ # Moreover, this module contains the method {Adyen::Form.redirect_signature_check} to
10
+ # check the request that is made to your website after the visitor has made his payment
11
+ # on the Adyen system for genuinity.
12
+ #
13
+ # You can use different skins in Adyen to define different payment environments. You can
14
+ # register these skins under a custom name in the module. The other methods will automatically
15
+ # use this information (i.e. the skin code and the shared secret) if it is available.
16
+ # Otherwise, you have to provide it yourself for every method call you make. See
17
+ # {Adyen::Form.register_skin} for more information.
18
+ #
19
+ # @see Adyen::Form.register_skin
20
+ # @see Adyen::Form.hidden_fields
21
+ # @see Adyen::Form.redirect_url
22
+ # @see Adyen::Form.redirect_signature_check
23
+ module Form
24
+
25
+ extend ActionView::Helpers::TagHelper
26
+
27
+ ######################################################
28
+ # SKINS
29
+ ######################################################
30
+
31
+ # Returns all registered skins and their accompanying skin code and shared secret.
32
+ # @return [Hash] The hash of registered skins.
33
+ def self.skins
34
+ @skins ||= {}
35
+ end
36
+
37
+ # Sets the registered skins.
38
+ # @param [Hash<Symbol, Hash>] hash A hash with the skin name as key and the skin parameter hash
39
+ # (which should include +:skin_code+ and +:shared_secret+) as value.
40
+ # @see Adyen::Form.register_skin
41
+ def self.skins=(hash)
42
+ @skins = hash.inject({}) do |skins, (name, skin)|
43
+ skins[name.to_sym] = skin.merge(:name => name.to_sym)
44
+ skins
45
+ end
46
+ end
47
+
48
+ # Registers a skin for later use.
49
+ #
50
+ # You can store a skin using a self defined symbol. Once the skin is registered,
51
+ # you can refer to it using this symbol instead of the hard-to-remember skin code.
52
+ # Moreover, the skin's shared_secret will be looked up automatically for calculting
53
+ # signatures.
54
+ #
55
+ # @example
56
+ # Adyen::Form.register_skin(:my_skin, 'dsfH67PO', 'Dfs*7uUln9')
57
+ # @param [Symbol] name The name of the skin.
58
+ # @param [String] skin_code The skin code for this skin, as defined by Adyen.
59
+ # @param [String] shared_secret The shared secret used for signature calculation.
60
+ # @see Adyen.load_config
61
+ def self.register_skin(name, skin_code, shared_secret)
62
+ self.skins[name.to_sym] = {:name => name.to_sym, :skin_code => skin_code, :shared_secret => shared_secret }
63
+ end
64
+
65
+ # Returns skin information given a skin name.
66
+ # @param [Symbol] skin_name The name of the skin
67
+ # @return [Hash, nil] A hash with the skin information, or nil if not found.
68
+ def self.skin_by_name(skin_name)
69
+ self.skins[skin_name.to_sym]
70
+ end
71
+
72
+ # Returns skin information given a skin code.
73
+ # @param [String] skin_code The skin code of the skin
74
+ # @return [Hash, nil] A hash with the skin information, or nil if not found.
75
+ def self.skin_by_code(skin_code)
76
+ self.skins.detect { |(name, skin)| skin[:skin_code] == skin_code }.last rescue nil
77
+ end
78
+
79
+ # Returns the shared secret belonging to a skin code.
80
+ # @param [String] skin_code The skin code of the skin
81
+ # @return [String, nil] The shared secret for the skin, or nil if not found.
82
+ def self.lookup_shared_secret(skin_code)
83
+ skin = skin_by_code(skin_code)[:shared_secret] rescue nil
84
+ end
85
+
86
+ ######################################################
87
+ # DEFAULT FORM / REDIRECT PARAMETERS
88
+ ######################################################
89
+
90
+ # Returns the default parameters to use, unless they are overridden.
91
+ # @see Adyen::Form.default_parameters
92
+ # @return [Hash] The hash of default parameters
93
+ def self.default_parameters
94
+ @default_arguments ||= {}
95
+ end
96
+
97
+ # Sets the default parameters to use.
98
+ # @see Adyen::Form.default_parameters
99
+ # @param [Hash] hash The hash of default parameters
100
+ def self.default_parameters=(hash)
101
+ @default_arguments = hash
102
+ end
103
+
104
+ ######################################################
105
+ # ADYEN FORM URL
106
+ ######################################################
107
+
108
+ # The URL of the Adyen payment system that still requires the current
109
+ # Adyen enviroment to be filled in.
110
+ ACTION_URL = "https://%s.adyen.com/hpp/select.shtml"
111
+
112
+ # Returns the URL of the Adyen payment system, adjusted for an Adyen environment.
113
+ #
114
+ # @param [String] environment The Adyen environment to use. This parameter can be
115
+ # left out, in which case the 'current' environment will be used.
116
+ # @return [String] The absolute URL of the Adyen payment system that can be used
117
+ # for payment forms or redirects.
118
+ # @see Adyen::Form.environment
119
+ # @see Adyen::Form.redirect_url
120
+ def self.url(environment = nil)
121
+ environment ||= Adyen.environment
122
+ Adyen::Form::ACTION_URL % environment.to_s
123
+ end
124
+
125
+ ######################################################
126
+ # POSTING/REDIRECTING TO ADYEN
127
+ ######################################################
128
+
129
+ # Transforms the payment parameters hash to be in the correct format.
130
+ # It will also include the default_parameters hash. Finally, switches
131
+ # the +:skin+ parameter out for the +:skin_code+ and +:shared_secret+
132
+ # parameter using the list of registered skins.
133
+ #
134
+ # @private
135
+ # @param [Hash] parameters The payment parameters hash to transform
136
+ def self.do_parameter_transformations!(parameters = {})
137
+ raise "YENs are not yet supported!" if parameters[:currency_code] == 'JPY' # TODO: fixme
138
+
139
+ parameters.replace(default_parameters.merge(parameters))
140
+ parameters[:recurring_contract] = 'RECURRING' if parameters.delete(:recurring) == true
141
+ parameters[:order_data] = Adyen::Encoding.gzip_base64(parameters.delete(:order_data_raw)) if parameters[:order_data_raw]
142
+ parameters[:ship_before_date] = Adyen::Formatter::DateTime.fmt_date(parameters[:ship_before_date])
143
+ parameters[:session_validity] = Adyen::Formatter::DateTime.fmt_time(parameters[:session_validity])
144
+
145
+ if parameters[:skin]
146
+ skin = Adyen::Form.skin_by_name(parameters.delete(:skin))
147
+ parameters[:skin_code] ||= skin[:skin_code]
148
+ parameters[:shared_secret] ||= skin[:shared_secret]
149
+ end
150
+ end
151
+
152
+ # Transforms the payment parameters to be in the correct format and calculates the merchant
153
+ # signature parameter. It also does some basic health checks on the parameters hash.
154
+ #
155
+ # @param [Hash] parameters The payment parameters. The parameters set in the
156
+ # {Adyen::Form.default_parameters} hash will be included automatically.
157
+ # @param [String] shared_secret The shared secret that should be used to calculate
158
+ # the payment request signature. This parameter can be left if the skin that is
159
+ # used is registered (see {Adyen::Form.register_skin}), or if the shared secret
160
+ # is provided as the +:shared_secret+ parameter.
161
+ # @return [Hash] The payment parameters with the +:merchant_signature+ parameter set.
162
+ # @raise [StandardError] Thrown if some parameter health check fails.
163
+ def self.payment_parameters(parameters = {}, shared_secret = nil)
164
+ do_parameter_transformations!(parameters)
165
+
166
+ raise "Cannot generate form: :currency code attribute not found!" unless parameters[:currency_code]
167
+ raise "Cannot generate form: :payment_amount code attribute not found!" unless parameters[:payment_amount]
168
+ raise "Cannot generate form: :merchant_account attribute not found!" unless parameters[:merchant_account]
169
+ raise "Cannot generate form: :skin_code attribute not found!" unless parameters[:skin_code]
170
+
171
+ # Calculate the merchant signature using the shared secret.
172
+ shared_secret ||= parameters.delete(:shared_secret)
173
+ raise "Cannot calculate payment request signature without shared secret!" unless shared_secret
174
+ parameters[:merchant_sig] = calculate_signature(parameters, shared_secret)
175
+
176
+ return parameters
177
+ end
178
+
179
+ # Returns an absolute URL to the Adyen payment system, with the payment parameters included
180
+ # as GET parameters in the URL. The URL also depends on the current Adyen enviroment.
181
+ #
182
+ # The payment parameters that are provided to this method will be merged with the
183
+ # {Adyen::Form.default_parameters} hash. The default parameter values will be overrided
184
+ # if another value is provided to this method.
185
+ #
186
+ # You do not have to provide the +:merchant_sig+ parameter: it will be calculated automatically
187
+ # if you provide either a registered skin name as the +:skin+ parameter or provide both the
188
+ # +:skin_code+ and +:shared_secret+ parameters.
189
+ #
190
+ # Note that Internet Explorer has a maximum length for URLs it can handle (2083 characters).
191
+ # Make sure that the URL is not longer than this limit if you want your site to work in IE.
192
+ #
193
+ # @example
194
+ #
195
+ # def pay
196
+ # # Genarate a URL to redirect to Adyen's payment system.
197
+ # adyen_url = Adyen::Form.redirect_url(:skin => :my_skin, :currency_code => 'USD',
198
+ # :payment_amount => 1000, merchant_account => 'MyMerchant', ... )
199
+ #
200
+ # respond_to do |format|
201
+ # format.html { redirect_to(adyen_url) }
202
+ # end
203
+ # end
204
+ #
205
+ # @param [Hash] parameters The payment parameters to include in the payment request.
206
+ # @return [String] An absolute URL to redirect to the Adyen payment system.
207
+ def self.redirect_url(parameters = {})
208
+ self.url + '?' + payment_parameters(parameters).map { |(k, v)|
209
+ "#{k.to_s.camelize(:lower)}=#{CGI.escape(v.to_s)}" }.join('&')
210
+ end
211
+
212
+ # Returns a HTML snippet of hidden INPUT tags with the provided payment parameters.
213
+ # The snippet can be included in a payment form that POSTs to the Adyen payment system.
214
+ #
215
+ # The payment parameters that are provided to this method will be merged with the
216
+ # {Adyen::Form.default_parameters} hash. The default parameter values will be overrided
217
+ # if another value is provided to this method.
218
+ #
219
+ # You do not have to provide the +:merchant_sig+ parameter: it will be calculated automatically
220
+ # if you provide either a registered skin name as the +:skin+ parameter or provide both the
221
+ # +:skin_code+ and +:shared_secret+ parameters.
222
+ #
223
+ # @example
224
+ # <% form_tag(Adyen::Form.url) do %>
225
+ # <%= Adyen::Form.hidden_fields(:skin => :my_skin, :currency_code => 'USD',
226
+ # :payment_amount => 1000, ...) %>
227
+ # <%= submit_tag("Pay invoice")
228
+ # <% end %>
229
+ #
230
+ # @param [Hash] parameters The payment parameters to include in the payment request.
231
+ # @return [String] An HTML snippet that can be included in a form that POSTs to the
232
+ # Adyen payment system.
233
+ def self.hidden_fields(parameters = {})
234
+
235
+ # Generate a hidden input tag per parameter, join them by newlines.
236
+ payment_parameters(parameters).map { |key, value|
237
+ self.tag(:input, :type => 'hidden', :name => key.to_s.camelize(:lower), :value => value)
238
+ }.join("\n")
239
+ end
240
+
241
+ ######################################################
242
+ # MERCHANT SIGNATURE CALCULATION
243
+ ######################################################
244
+
245
+ # Generates the string that is used to calculate the request signature. This signature
246
+ # is used by Adyen to check whether the request is genuinely originating from you.
247
+ # @param [Hash] parameters The parameters that will be included in the payment request.
248
+ # @return [String] The string for which the siganture is calculated.
249
+ def self.calculate_signature_string(parameters)
250
+ merchant_sig_string = ""
251
+ merchant_sig_string << parameters[:payment_amount].to_s << parameters[:currency_code].to_s <<
252
+ parameters[:ship_before_date].to_s << parameters[:merchant_reference].to_s <<
253
+ parameters[:skin_code].to_s << parameters[:merchant_account].to_s <<
254
+ parameters[:session_validity].to_s << parameters[:shopper_email].to_s <<
255
+ parameters[:shopper_reference].to_s << parameters[:recurring_contract].to_s <<
256
+ parameters[:allowed_methods].to_s << parameters[:blocked_methods].to_s <<
257
+ parameters[:shopper_statement].to_s << parameters[:billing_address_type].to_s
258
+ end
259
+
260
+ # Calculates the payment request signature for the given payment parameters.
261
+ #
262
+ # This signature is used by Adyen to check whether the request is
263
+ # genuinely originating from you. The resulting signature should be
264
+ # included in the payment request parameters as the +merchantSig+
265
+ # parameter; the shared secret should of course not be included.
266
+ #
267
+ # @param [Hash] parameters The payment parameters for which to calculate
268
+ # the payment request signature.
269
+ # @param [String] shared_secret The shared secret to use for this signature.
270
+ # It should correspond with the skin_code parameter. This parameter can be
271
+ # left out if the shared_secret is included as key in the parameters.
272
+ # @return [String] The signature of the payment request
273
+ def self.calculate_signature(parameters, shared_secret = nil)
274
+ shared_secret ||= parameters.delete(:shared_secret)
275
+ Adyen::Encoding.hmac_base64(shared_secret, calculate_signature_string(parameters))
276
+ end
277
+
278
+ ######################################################
279
+ # REDIRECT SIGNATURE CHECKING
280
+ ######################################################
281
+
282
+ # Generates the string for which the redirect signature is calculated, using the request paramaters.
283
+ # @param [Hash] params A hash of HTTP GET parameters for the redirect request.
284
+ # @return [String] The signature string.
285
+ def self.redirect_signature_string(params)
286
+ params[:authResult].to_s + params[:pspReference].to_s + params[:merchantReference].to_s + params[:skinCode].to_s
287
+ end
288
+
289
+ # Computes the redirect signature using the request parameters, so that the
290
+ # redirect can be checked for forgery.
291
+ #
292
+ # @param [Hash] params A hash of HTTP GET parameters for the redirect request.
293
+ # @param [String] shared_secret The shared secret for the Adyen skin that was used for
294
+ # the original payment form. You can leave this out of the skin is registered
295
+ # using the {Adyen::Form.register_skin} method.
296
+ # @return [String] The redirect signature
297
+ def self.redirect_signature(params, shared_secret = nil)
298
+ shared_secret ||= lookup_shared_secret(params[:skinCode])
299
+ Adyen::Encoding.hmac_base64(shared_secret, redirect_signature_string(params))
300
+ end
301
+
302
+ # Checks the redirect signature for this request by calcultating the signature from
303
+ # the provided parameters, and comparing it to the signature provided in the +merchantSig+
304
+ # parameter.
305
+ #
306
+ # If this method returns false, the request could be a forgery and should not be handled.
307
+ # Therefore, you should include this check in a +before_filter+, and raise an error of the
308
+ # signature check fails.
309
+ #
310
+ # @example
311
+ # class PaymentsController < ApplicationController
312
+ # before_filter :check_signature, :only => [:return_from_adyen]
313
+ #
314
+ # def return_from_adyen
315
+ # @invoice = Invoice.find(params[:merchantReference])
316
+ # @invoice.set_paid! if params[:authResult] == 'AUTHORISED'
317
+ # end
318
+ #
319
+ # private
320
+ #
321
+ # def check_signature
322
+ # raise "Forgery!" unless Adyen::Form.redirect_signature_check(params)
323
+ # end
324
+ # end
325
+ #
326
+ # @param [Hash] params params A hash of HTTP GET parameters for the redirect request. This
327
+ # should include the +:merchantSig+ parameter, which contains the signature.
328
+ # @param [String] shared_secret The shared secret for the Adyen skin that was used for
329
+ # the original payment form. You can leave this out of the skin is registered
330
+ # using the {Adyen::Form.register_skin} method.
331
+ # @return [true, false] Returns true only if the signature in the parameters is correct.
332
+ def self.redirect_signature_check(params, shared_secret = nil)
333
+ params[:merchantSig] == redirect_signature(params, shared_secret)
334
+ end
335
+ end
336
+ end
@@ -0,0 +1,37 @@
1
+ module Adyen
2
+ module Formatter
3
+ module DateTime
4
+ # Returns a valid Adyen string representation for a date
5
+ def self.fmt_date(date)
6
+ case date
7
+ when Date, DateTime, Time
8
+ date.strftime('%Y-%m-%d')
9
+ else
10
+ raise "Invalid date notation: #{date.inspect}!" unless /^\d{4}-\d{2}-\d{2}$/ =~ date
11
+ date
12
+ end
13
+ end
14
+
15
+ # Returns a valid Adyen string representation for a timestamp
16
+ def self.fmt_time(time)
17
+ case time
18
+ when Date, DateTime, Time
19
+ time.strftime('%Y-%m-%dT%H:%M:%SZ')
20
+ else
21
+ raise "Invalid timestamp notation: #{time.inspect}!" unless /^\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}Z$/ =~ time
22
+ time
23
+ end
24
+ end
25
+ end
26
+
27
+ module Price
28
+ def self.in_cents(price)
29
+ ((price * 100).round).to_i
30
+ end
31
+
32
+ def self.from_cents(price)
33
+ BigDecimal.new(price.to_s) / 100
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,105 @@
1
+ require 'xml'
2
+
3
+ module Adyen
4
+ module Matchers
5
+
6
+ module XPathPaymentFormCheck
7
+
8
+ def self.build_xpath_query(checks)
9
+ # Start by finding the check for the Adyen form tag
10
+ xpath_query = "//form[@action='#{Adyen::Form.url}']"
11
+
12
+ # Add recurring/single check if specified
13
+ recurring = checks.delete(:recurring)
14
+ unless recurring.nil?
15
+ if recurring
16
+ xpath_query << "[descendant::input[@type='hidden'][@name='recurringContract']]"
17
+ else
18
+ xpath_query << "[not(descendant::input[@type='hidden'][@name='recurringContract'])]"
19
+ end
20
+ end
21
+
22
+ # Add a check for all the other fields specified
23
+ checks.each do |key, value|
24
+ condition = "descendant::input[@type='hidden'][@name='#{key.to_s.camelize(:lower)}']"
25
+ condition << "[@value='#{value}']" unless value == :anything
26
+ xpath_query << "[#{condition}]"
27
+ end
28
+
29
+ return xpath_query
30
+ end
31
+
32
+ def self.document(subject)
33
+ if String === subject
34
+ XML::HTMLParser.string(subject).parse
35
+ elsif subject.respond_to?(:body)
36
+ XML::HTMLParser.string(subject.body).parse
37
+ elsif XML::Node === subject
38
+ subject
39
+ elsif XML::Document === subject
40
+ subject
41
+ else
42
+ raise "Cannot handle this XML input type"
43
+ end
44
+ end
45
+
46
+ def self.check(subject, checks = {})
47
+ document(subject).find_first(build_xpath_query(checks))
48
+ end
49
+ end
50
+
51
+ class HaveAdyenPaymentForm
52
+
53
+ def initialize(checks)
54
+ @checks = checks
55
+ end
56
+
57
+ def matches?(document)
58
+ Adyen::Matchers::XPathPaymentFormCheck.check(document, @checks)
59
+ end
60
+
61
+ def description
62
+ "have an adyen payment form"
63
+ end
64
+
65
+ def failure_message
66
+ "expected to find a valid Adyen form on this page"
67
+ end
68
+
69
+ def negative_failure_message
70
+ "expected not to find a valid Adyen form on this page"
71
+ end
72
+ end
73
+
74
+ def have_adyen_payment_form(checks = {})
75
+ default_checks = {:merchant_sig => :anything, :payment_amount => :anything, :currency_code => :anything, :skin_code => :anything }
76
+ HaveAdyenPaymentForm.new(default_checks.merge(checks))
77
+ end
78
+
79
+ def have_adyen_recurring_payment_form(checks = {})
80
+ recurring_checks = { :recurring => true, :shopper_email => :anything, :shopper_reference => :anything }
81
+ have_adyen_payment_form(recurring_checks.merge(checks))
82
+ end
83
+
84
+ def have_adyen_single_payment_form(checks = {})
85
+ recurring_checks = { :recurring => false }
86
+ have_adyen_payment_form(recurring_checks.merge(checks))
87
+ end
88
+
89
+ def assert_adyen_payment_form(subject, checks = {})
90
+ default_checks = {:merchant_sig => :anything, :payment_amount => :anything, :currency_code => :anything, :skin_code => :anything }
91
+ assert Adyen::Matchers::XPathPaymentFormCheck.check(subject, default_checks.merge(checks)), 'No Adyen payment form found'
92
+ end
93
+
94
+ def assert_adyen_recurring_payment_form(subject, checks = {})
95
+ recurring_checks = { :recurring => true, :shopper_email => :anything, :shopper_reference => :anything }
96
+ assert_adyen_payment_form(subject, recurring_checks.merge(checks))
97
+ end
98
+
99
+ def assert_adyen_single_payment_form(subject, checks = {})
100
+ recurring_checks = { :recurring => false }
101
+ assert_adyen_payment_form(subject, recurring_checks.merge(checks))
102
+ end
103
+
104
+ end
105
+ end