amazon_associate 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ module AmazonAssociate
2
+ class ConfigurationError < RuntimeError
3
+ end
4
+ end
@@ -0,0 +1,100 @@
1
+ # Internal wrapper class to provide convenient method to access Hpricot element value.
2
+ module AmazonAssociate
3
+ class Element
4
+ # Pass Hpricot::Elements object
5
+ def initialize(element)
6
+ @element = element
7
+ end
8
+
9
+ # Returns Hpricot::Elments object
10
+ def elem
11
+ @element
12
+ end
13
+
14
+ # Find Hpricot::Elements matching the given path. Example: element/"author".
15
+ def /(path)
16
+ elements = @element/path
17
+ return nil if elements.size == 0
18
+ elements
19
+ end
20
+
21
+ # Find Hpricot::Elements matching the given path, and convert to AmazonAssociate::Element.
22
+ # Returns an array AmazonAssociate::Elements if more than Hpricot::Elements size is greater than 1.
23
+ def search_and_convert(path)
24
+ elements = self./(path)
25
+ return unless elements
26
+ elements = elements.map{|element| Element.new(element)}
27
+ return elements.first if elements.size == 1
28
+ elements
29
+ end
30
+
31
+ # Get the text value of the given path, leave empty to retrieve current element value.
32
+ def get(path="")
33
+ Element.get(@element, path)
34
+ end
35
+
36
+ # Get the unescaped HTML text of the given path.
37
+ def get_unescaped(path="")
38
+ Element.get_unescaped(@element, path)
39
+ end
40
+
41
+ # Get the array values of the given path.
42
+ def get_array(path="")
43
+ Element.get_array(@element, path)
44
+ end
45
+
46
+ # Get the children element text values in hash format with the element names as the hash keys.
47
+ def get_hash(path="")
48
+ Element.get_hash(@element, path)
49
+ end
50
+
51
+ # Similar to #get, except an element object must be passed-in.
52
+ def self.get(element, path="")
53
+ return unless element
54
+ result = element.at(path)
55
+ result = result.inner_html if result
56
+ result
57
+ end
58
+
59
+ # Similar to #get_unescaped, except an element object must be passed-in.
60
+ def self.get_unescaped(element, path="")
61
+ result = get(element, path)
62
+ CGI::unescapeHTML(result) if result
63
+ end
64
+
65
+ # Similar to #get_array, except an element object must be passed-in.
66
+ def self.get_array(element, path="")
67
+ return unless element
68
+
69
+ result = element/path
70
+ if (result.is_a? Hpricot::Elements) || (result.is_a? Array)
71
+ parsed_result = []
72
+ result.each {|item|
73
+ parsed_result << Element.get(item)
74
+ }
75
+ parsed_result
76
+ else
77
+ [Element.get(result)]
78
+ end
79
+ end
80
+
81
+ # Similar to #get_hash, except an element object must be passed-in.
82
+ def self.get_hash(element, path="")
83
+ return unless element
84
+
85
+ result = element.at(path)
86
+ if result
87
+ hash = {}
88
+ result = result.children
89
+ result.each do |item|
90
+ hash[item.name.to_sym] = item.inner_html
91
+ end
92
+ hash
93
+ end
94
+ end
95
+
96
+ def to_s
97
+ elem.to_s if elem
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,354 @@
1
+ require "net/http"
2
+ require "hpricot"
3
+ require "cgi"
4
+
5
+ begin
6
+ require 'md5'
7
+ rescue LoadError
8
+ require 'digest/md5'
9
+ end
10
+
11
+ #--
12
+ # Copyright (c) 2009 Dan Pickett, Enlight Solutions
13
+ #
14
+ # Permission is hereby granted, free of charge, to any person obtaining
15
+ # a copy of this software and associated documentation files (the
16
+ # "Software"), to deal in the Software without restriction, including
17
+ # without limitation the rights to use, copy, modify, merge, publish,
18
+ # distribute, sublicense, and/or sell copies of the Software, and to
19
+ # permit persons to whom the Software is furnished to do so, subject to
20
+ # the following conditions:
21
+ #
22
+ # The above copyright notice and this permission notice shall be
23
+ # included in all copies or substantial portions of the Software.
24
+ #
25
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
26
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
27
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
28
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
29
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
30
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
31
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32
+ #++
33
+ module AmazonAssociate
34
+ class Request
35
+
36
+ SERVICE_URLS = {:us => "http://webservices.amazon.com",
37
+ :uk => "http://webservices.amazon.co.uk",
38
+ :ca => "http://webservices.amazon.ca",
39
+ :de => "http://webservices.amazon.de",
40
+ :jp => "http://webservices.amazon.co.jp",
41
+ :fr => "http://webservices.amazon.fr"
42
+ }
43
+
44
+ # The sort types available to each product search index.
45
+ SORT_TYPES = {
46
+ "Apparel" => %w[relevancerank salesrank pricerank inverseprice -launch-date sale-flag],
47
+ "Automotive" => %w[salesrank price -price titlerank -titlerank],
48
+ "Baby" => %w[psrank salesrank price -price titlerank],
49
+ "Beauty" => %w[pmrank salesrank price -price -launch-date sale-flag],
50
+ "Books" => %w[relevancerank salesrank reviewrank pricerank inverse-pricerank daterank titlerank -titlerank],
51
+ "Classical" => %w[psrank salesrank price -price titlerank -titlerank orig-rel-date],
52
+ "DigitalMusic" => %w[songtitlerank uploaddaterank],
53
+ "DVD" => %w[relevancerank salesrank price -price titlerank -video-release-date],
54
+ "Electronics" => %w[pmrank salesrank reviewrank price -price titlerank],
55
+ "GourmetFood" => %w[relevancerank salesrank pricerank inverseprice launch-date sale-flag],
56
+ "HealthPersonalCare" => %w[pmrank salesrank pricerank inverseprice launch-date sale-flag],
57
+ "Jewelry" => %w[pmrank salesrank pricerank inverseprice launch-date],
58
+ "Kitchen" => %w[pmrank salesrank price -price titlerank -titlerank],
59
+ "Magazines" => %w[subslot-salesrank reviewrank price -price daterank titlerank -titlerank],
60
+ "Merchants" => %w[relevancerank salesrank pricerank inverseprice launch-date sale-flag],
61
+ "Miscellaneous" => %w[pmrank salesrank price -price titlerank -titlerank],
62
+ "Music" => %w[psrank salesrank price -price titlerank -titlerank artistrank orig-rel-date release-date],
63
+ "MusicalInstruments" => %w[pmrank salesrank price -price -launch-date sale-flag],
64
+ "MusicTracks" => %w[titlerank -titlerank],
65
+ "OfficeProducts" => %w[pmrank salesrank reviewrank price -price titlerank],
66
+ "OutdoorLiving" => %w[psrank salesrank price -price titlerank -titlerank],
67
+ "PCHardware" => %w[psrank salesrank price -price titlerank],
68
+ "PetSupplies" => %w[+pmrank salesrank price -price titlerank -titlerank],
69
+ "Photo" => %w[pmrank salesrank titlerank -titlerank],
70
+ "Restaurants" => %w[relevancerank titlerank],
71
+ "Software" => %w[pmrank salesrank titlerank price -price],
72
+ "SportingGoods" => %w[relevancerank salesrank pricerank inverseprice launch-date sale-flag],
73
+ "Tools" => %w[pmrank salesrank titlerank -titlerank price -price],
74
+ "Toys" => %w[pmrank salesrank price -price titlerank -age-min],
75
+ "VHS" => %w[relevancerank salesrank price -price titlerank -video-release-date],
76
+ "Video" => %w[relevancerank salesrank price -price titlerank -video-release-date],
77
+ "VideoGames" => %w[pmrank salesrank price -price titlerank],
78
+ "Wireless" => %w[daterank pricerank invers-pricerank reviewrank salesrank titlerank -titlerank],
79
+ "WirelessAccessories" => %w[psrank salesrank titlerank -titlerank]
80
+ }
81
+
82
+ # Returns an Array of valid sort types for _search_index_, or +nil+ if _search_index_ is invalid.
83
+ def self.sort_types(search_index)
84
+ SORT_TYPES.has_key?(search_index) ? SORT_TYPES[search_index] : nil
85
+ end
86
+
87
+ # Performs BrowseNodeLookup request, defaults to TopSellers ResponseGroup
88
+ def self.browse_node_lookup(browse_node_id, opts = {})
89
+ opts = self.options.merge(opts) if self.options
90
+ opts[:operation] = "BrowseNodeLookup"
91
+ opts[:browse_node_id] = browse_node_id
92
+
93
+ self.send_request(opts)
94
+ end
95
+
96
+ # Cart operations build the Item tags from the ASIN
97
+ # Item.ASIN.Quantity defaults to 1, unless otherwise specified in _opts_
98
+
99
+ # Creates remote shopping cart containing _asin_
100
+ def self.cart_create(items, opts = {})
101
+ opts = self.options.merge(opts) if self.options
102
+ opts[:operation] = "CartCreate"
103
+
104
+ if items.is_a?(String)
105
+ asin = items
106
+ opts["Item.#{asin}.Quantity"] = opts[:quantity] || 1
107
+ opts["Item.#{asin}.ASIN"] = asin
108
+ else
109
+ items.each do |item|
110
+ (item[:offer_listing_id].nil? || item[:offer_listing_id].empty?) ? opts["Item.#{item[:asin]}.ASIN"] = item[:asin] : opts["Item.#{item[:asin]}.OfferListingId"] = item[:offer_listing_id]
111
+ opts["Item.#{item[:asin]}.Quantity"] = item[:quantity] || 1
112
+ end
113
+ end
114
+
115
+ self.send_request(opts)
116
+ end
117
+
118
+ # Adds items to remote shopping cart
119
+ def self.cart_add(items, cart_id, hmac, opts = {})
120
+ opts = self.options.merge(opts) if self.options
121
+ opts[:operation] = "CartAdd"
122
+
123
+ if items.is_a?(String)
124
+ asin = items
125
+ opts["Item.#{asin}.Quantity"] = opts[:quantity] || 1
126
+ opts["Item.#{asin}.ASIN"] = asin
127
+ else
128
+ items.each do |item|
129
+ (item[:offer_listing_id].nil? || item[:offer_listing_id].empty?) ? opts["Item.#{item[:asin]}.ASIN"] = item[:asin] : opts["Item.#{item[:asin]}.OfferListingId"] = item[:offer_listing_id]
130
+ opts["Item.#{item[:asin]}.Quantity"] = item[:quantity] || 1
131
+ end
132
+ end
133
+
134
+ opts[:cart_id] = cart_id
135
+ opts[:hMAC] = hmac
136
+
137
+ self.send_request(opts)
138
+ end
139
+
140
+ # Retrieve a remote shopping cart
141
+ def self.cart_get(cart_id, hmac, opts = {})
142
+ opts = self.options.merge(opts) if self.options
143
+ opts[:operation] = "CartGet"
144
+ opts[:cart_id] = cart_id
145
+ opts[:hMAC] = hmac
146
+
147
+ self.send_request(opts)
148
+ end
149
+
150
+ # modifies _cart_item_id_ in remote shopping cart
151
+ # _quantity_ defaults to 0 to remove the given _cart_item_id_
152
+ # specify _quantity_ to update cart contents
153
+ def self.cart_modify(cart_item_id, cart_id, hmac, quantity=0, opts = {})
154
+ opts = self.options.merge(opts) if self.options
155
+ opts[:operation] = "CartModify"
156
+ opts["Item.1.CartItemId"] = cart_item_id
157
+ opts["Item.1.Quantity"] = quantity
158
+ opts[:cart_id] = cart_id
159
+ opts[:hMAC] = hmac
160
+
161
+ self.send_request(opts)
162
+ end
163
+
164
+ # clears contents of remote shopping cart
165
+ def self.cart_clear(cart_id, hmac, opts = {})
166
+ opts = self.options.merge(opts) if self.options
167
+ opts[:operation] = "CartClear"
168
+ opts[:cart_id] = cart_id
169
+ opts[:hMAC] = hmac
170
+
171
+ self.send_request(opts)
172
+ end
173
+ @@options = {}
174
+ @@debug = false
175
+
176
+ # Default search options
177
+ def self.options
178
+ @@options
179
+ end
180
+
181
+ # Set default search options
182
+ def self.options=(opts)
183
+ @@options = opts
184
+ end
185
+
186
+ # Get debug flag.
187
+ def self.debug
188
+ @@debug
189
+ end
190
+
191
+ # Set debug flag to true or false.
192
+ def self.debug=(dbg)
193
+ @@debug = dbg
194
+ end
195
+
196
+ def self.configure(&proc)
197
+ raise ArgumentError, "Block is required." unless block_given?
198
+
199
+ yield @@options
200
+ if !@@options[:caching_strategy].nil?
201
+ @@options.merge!(CacheFactory.initialize_options(@@options))
202
+ end
203
+ end
204
+
205
+ # Search amazon items with search terms. Default search index option is "Books".
206
+ # For other search type other than keywords, please specify :type => [search type param name].
207
+ def self.item_search(terms, opts = {})
208
+ opts[:operation] = "ItemSearch"
209
+ opts[:search_index] = opts[:search_index] || "Books"
210
+
211
+ type = opts.delete(:type)
212
+ if type
213
+ opts[type.to_sym] = terms
214
+ else
215
+ opts[:keywords] = terms
216
+ end
217
+
218
+ self.send_request(opts)
219
+ end
220
+
221
+ # Search an item by ASIN no.
222
+ def self.item_lookup(item_id, opts = {})
223
+ opts[:operation] = "ItemLookup"
224
+ opts[:item_id] = item_id
225
+
226
+ self.send_request(opts)
227
+ end
228
+
229
+ # Generic send request to ECS REST service. You have to specify the :operation parameter.
230
+ def self.send_request(opts)
231
+ opts = self.options.merge(opts) if self.options
232
+ unsigned_url = prepare_unsigned_url(opts)
233
+ response = nil
234
+
235
+ if caching_enabled?
236
+ AmazonAssociate::CacheFactory.sweep(self.options[:caching_strategy])
237
+
238
+ res = AmazonAssociate::CacheFactory.get(unsigned_url, self.options[:caching_strategy])
239
+ response = Response.new(res, unsigned_url) unless res.nil?
240
+ end
241
+
242
+ if !caching_enabled? || response.nil?
243
+ request_url = prepare_signed_url(opts)
244
+ log "Request URL: #{request_url}"
245
+ res = Net::HTTP.get_response(URI::parse(request_url))
246
+
247
+ unless res.kind_of? Net::HTTPSuccess
248
+ raise AmazonAssociate::RequestError, "HTTP Response: #{res.code} #{res.message}"
249
+ end
250
+
251
+ response = Response.new(res.body, request_url)
252
+ response.unsigned_url = unsigned_url
253
+
254
+ if caching_enabled?
255
+ cache_response(unsigned_url, response, self.options[:caching_strategy])
256
+ end
257
+ end
258
+
259
+ response
260
+ end
261
+
262
+ attr_accessor :request_url, :unsigned_url
263
+
264
+ protected
265
+ def self.log(s)
266
+ return unless self.debug
267
+ if defined? RAILS_DEFAULT_LOGGER
268
+ RAILS_DEFAULT_LOGGER.error(s)
269
+ elsif defined? LOGGER
270
+ LOGGER.error(s)
271
+ else
272
+ puts s
273
+ end
274
+ end
275
+
276
+ private
277
+ def self.get_service_url(opts)
278
+ country = opts.delete(:country)
279
+ country = (country.nil?) ? "us" : country
280
+ url = SERVICE_URLS[country.to_sym]
281
+
282
+ raise AmazonAssociate::RequestError, "Invalid country \"#{country}\"" unless url
283
+ url
284
+ end
285
+
286
+ def self.prepare_unsigned_url(opts)
287
+ url = get_service_url(opts) + "/onca/xml"
288
+
289
+ qs = ""
290
+ opts.each {|k,v|
291
+ next unless v
292
+ next if [:caching_options, :caching_strategy, :secret_key].include?(k)
293
+ v = v.join(",") if v.is_a? Array
294
+ qs << "&#{camelize(k.to_s)}=#{URI.encode(v.to_s)}"
295
+ }
296
+
297
+ @unsigned_url = "#{url}#{qs}"
298
+ end
299
+
300
+ def self.prepare_signed_url(opts)
301
+ url = get_service_url(opts) + "/onca/xml"
302
+
303
+ unencoded_key_value_strings = []
304
+ encoded_key_value_strings = []
305
+ opts[:timestamp] = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S") + ".000Z"
306
+ opts[:service] = "AWSECommerceService"
307
+ opts[:version] = "2009-01-01"
308
+ sort_parameters(opts).each do |p|
309
+ next if p[1].nil?
310
+ next if [:caching_options, :caching_strategy, :secret_key].include?(p[0])
311
+
312
+
313
+ encoded_value = CGI.escape(p[1].to_s)
314
+
315
+ encoded_key_value_strings << camelize(p[0].to_s ) + "=" + encoded_value
316
+ end
317
+
318
+ string_to_sign =
319
+ "GET
320
+ #{get_service_url(opts).gsub("http://", "")}
321
+ /onca/xml
322
+ #{encoded_key_value_strings.join("&")}"
323
+
324
+ signature = sign_string(string_to_sign)
325
+ encoded_key_value_strings << "Signature=" + signature
326
+
327
+ "#{url}?#{encoded_key_value_strings.join("&")}"
328
+ end
329
+
330
+ def self.sort_parameters(opts)
331
+ key_value_strings = []
332
+ opts.sort {|a, b| camelize(a) <=> camelize(b) }
333
+ end
334
+
335
+ def self.sign_string(string_to_sign)
336
+ sha1 = HMAC::SHA256.digest(self.options[:secret_key], string_to_sign)
337
+
338
+ #Base64 encoding adds a linefeed to the end of the string so chop the last character!
339
+ CGI.escape(Base64.encode64(sha1).chomp)
340
+ end
341
+
342
+ def self.camelize(s)
343
+ s.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
344
+ end
345
+
346
+ def self.caching_enabled?
347
+ !self.options[:caching_strategy].nil?
348
+ end
349
+
350
+ def self.cache_response(request, response, options)
351
+ AmazonAssociate::CacheFactory.cache(request, response, options)
352
+ end
353
+ end
354
+ end