alexandria-book-collection-manager 0.7.8 → 0.7.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (134) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +48 -53
  3. data/.rubocop.yml +18 -5
  4. data/.rubocop_todo.yml +31 -37
  5. data/.simplecov +2 -2
  6. data/CHANGELOG.md +37 -0
  7. data/ChangeLog.0 +19 -19
  8. data/INSTALL.md +3 -5
  9. data/README.md +0 -5
  10. data/Rakefile +14 -14
  11. data/alexandria-book-collection-manager.gemspec +35 -34
  12. data/doc/FAQ +2 -2
  13. data/lib/alexandria/about.rb +1 -1
  14. data/lib/alexandria/book_providers/bl_provider.rb +88 -0
  15. data/lib/alexandria/book_providers/loc_provider.rb +38 -0
  16. data/lib/alexandria/book_providers/sbn_provider.rb +108 -0
  17. data/lib/alexandria/book_providers/thalia_provider.rb +1 -1
  18. data/lib/alexandria/book_providers/web.rb +2 -2
  19. data/lib/alexandria/book_providers/worldcat.rb +9 -7
  20. data/lib/alexandria/book_providers/z3950_provider.rb +199 -0
  21. data/lib/alexandria/book_providers.rb +10 -25
  22. data/lib/alexandria/console.rb +2 -2
  23. data/lib/alexandria/default_preferences.rb +1 -1
  24. data/lib/alexandria/export_library.rb +14 -14
  25. data/lib/alexandria/image_fetcher.rb +25 -0
  26. data/lib/alexandria/import_library.rb +10 -10
  27. data/lib/alexandria/library_store.rb +4 -5
  28. data/lib/alexandria/models/book.rb +13 -0
  29. data/lib/alexandria/models/library.rb +15 -23
  30. data/lib/alexandria/preferences.rb +4 -6
  31. data/lib/alexandria/{book_providers/pseudomarc.rb → pseudo_marc_parser.rb} +2 -2
  32. data/lib/alexandria/scanners/cue_cat.rb +1 -1
  33. data/lib/alexandria/smart_library.rb +2 -2
  34. data/lib/alexandria/ui/about_dialog.rb +1 -1
  35. data/lib/alexandria/ui/acquire_dialog.rb +6 -9
  36. data/lib/alexandria/ui/alert_dialog.rb +2 -2
  37. data/lib/alexandria/ui/barcode_animation.rb +1 -1
  38. data/lib/alexandria/ui/book_properties_dialog_base.rb +5 -9
  39. data/lib/alexandria/ui/completion_models.rb +1 -5
  40. data/lib/alexandria/ui/conflict_while_copying_dialog.rb +1 -1
  41. data/lib/alexandria/ui/icons.rb +2 -2
  42. data/lib/alexandria/ui/init.rb +10 -4
  43. data/lib/alexandria/ui/listview.rb +1 -1
  44. data/lib/alexandria/ui/multi_drag_treeview.rb +1 -1
  45. data/lib/alexandria/ui/new_book_dialog.rb +11 -13
  46. data/lib/alexandria/ui/new_book_dialog_manual.rb +1 -1
  47. data/lib/alexandria/ui/preferences_dialog.rb +2 -2
  48. data/lib/alexandria/ui/provider_preferences_base_dialog.rb +1 -1
  49. data/lib/alexandria/ui/really_delete_dialog.rb +1 -1
  50. data/lib/alexandria/ui/ui_manager.rb +17 -25
  51. data/lib/alexandria/version.rb +1 -1
  52. data/lib/alexandria/web_themes.rb +1 -1
  53. data/lib/alexandria.rb +6 -5
  54. data/po/cs.po +90 -125
  55. data/po/cy.po +87 -125
  56. data/po/de.po +96 -125
  57. data/po/el.po +96 -125
  58. data/po/es.po +96 -125
  59. data/po/fr.po +90 -125
  60. data/po/ga.po +83 -124
  61. data/po/gl.po +90 -125
  62. data/po/it.po +90 -125
  63. data/po/ja.po +90 -125
  64. data/po/mk.po +96 -125
  65. data/po/nb.po +90 -125
  66. data/po/nl.po +107 -124
  67. data/po/pl.po +113 -124
  68. data/po/pt.po +90 -125
  69. data/po/pt_BR.po +90 -125
  70. data/po/ru.po +92 -124
  71. data/po/sk.po +90 -125
  72. data/po/sv.po +90 -125
  73. data/po/uk.po +90 -125
  74. data/po/zh_TW.po +90 -125
  75. data/schemas/alexandria.schemas +1 -1
  76. data/share/gnome/help/alexandria/C/adding-books.xml +3 -4
  77. data/share/gnome/help/alexandria/C/introduction.xml +0 -16
  78. data/share/gnome/help/alexandria/C/searching.xml +1 -4
  79. data/share/gnome/help/alexandria/C/settings.xml +0 -30
  80. data/share/gnome/help/alexandria/fr/alexandria.xml +4 -159
  81. data/share/gnome/help/alexandria/ja/adding-books.xml +1 -1
  82. data/share/gnome/help/alexandria/ja/introduction.xml +0 -15
  83. data/share/gnome/help/alexandria/ja/searching.xml +3 -7
  84. data/share/gnome/help/alexandria/ja/settings.xml +0 -27
  85. data/spec/alexandria/book_providers/bl_provider_spec.rb +13 -0
  86. data/spec/alexandria/book_providers/loc_provider_spec.rb +17 -0
  87. data/spec/alexandria/book_providers/sbn_provider_spec.rb +13 -0
  88. data/spec/alexandria/book_providers/thalia_provider_spec.rb +1 -1
  89. data/spec/alexandria/book_providers/world_cat_provider_spec.rb +22 -10
  90. data/spec/alexandria/book_providers_spec.rb +0 -81
  91. data/spec/alexandria/book_spec.rb +5 -3
  92. data/spec/alexandria/export_library_spec.rb +8 -8
  93. data/spec/alexandria/library_spec.rb +83 -51
  94. data/spec/alexandria/library_store_spec.rb +1 -1
  95. data/spec/alexandria/preferences_spec.rb +7 -7
  96. data/spec/alexandria/pseudo_marc_parser_spec.rb +71 -0
  97. data/spec/alexandria/scanners/cue_cat_spec.rb +11 -4
  98. data/spec/alexandria/smart_library_spec.rb +7 -5
  99. data/spec/alexandria/ui/about_dialog_spec.rb +1 -1
  100. data/spec/alexandria/ui/acquire_dialog_spec.rb +1 -1
  101. data/spec/alexandria/ui/alert_dialog_spec.rb +5 -3
  102. data/spec/alexandria/ui/bad_isbns_dialog_spec.rb +1 -1
  103. data/spec/alexandria/ui/book_properties_dialog_spec.rb +4 -4
  104. data/spec/alexandria/ui/confirm_erase_dialog_spec.rb +18 -2
  105. data/spec/alexandria/ui/conflict_while_copying_dialog_spec.rb +1 -1
  106. data/spec/alexandria/ui/error_dialog_spec.rb +13 -2
  107. data/spec/alexandria/ui/export_dialog_spec.rb +3 -3
  108. data/spec/alexandria/ui/iconview_spec.rb +1 -1
  109. data/spec/alexandria/ui/import_dialog_spec.rb +3 -3
  110. data/spec/alexandria/ui/keep_bad_isbn_dialog_spec.rb +1 -1
  111. data/spec/alexandria/ui/new_book_dialog_manual_spec.rb +4 -4
  112. data/spec/alexandria/ui/new_book_dialog_spec.rb +2 -2
  113. data/spec/alexandria/ui/new_provider_dialog_spec.rb +3 -3
  114. data/spec/alexandria/ui/new_smart_library_dialog_spec.rb +9 -7
  115. data/spec/alexandria/ui/preferences_dialog_spec.rb +1 -1
  116. data/spec/alexandria/ui/provider_preferences_dialog_spec.rb +2 -2
  117. data/spec/alexandria/ui/really_delete_dialog_spec.rb +1 -1
  118. data/spec/alexandria/ui/sidepane_manager_spec.rb +1 -1
  119. data/spec/alexandria/ui/skip_entry_dialog_spec.rb +18 -2
  120. data/spec/alexandria/ui/smart_library_properties_dialog_spec.rb +2 -2
  121. data/spec/alexandria/ui/ui_manager_spec.rb +83 -5
  122. data/spec/data/libraries/0.6.2/My Library/9780571147168.yaml +2 -0
  123. data/spec/spec_helper.rb +23 -32
  124. data/util/rake/fileinstall.rb +12 -12
  125. data/util/rake/gettextgenerate.rb +1 -1
  126. data/util/rake/omfgenerate.rb +1 -1
  127. metadata +73 -58
  128. data/lib/alexandria/book_providers/adlibris.rb +0 -191
  129. data/lib/alexandria/book_providers/amazon_aws.rb +0 -239
  130. data/lib/alexandria/book_providers/amazon_ecs_util.rb +0 -373
  131. data/lib/alexandria/book_providers/barnes_and_noble.rb +0 -209
  132. data/lib/alexandria/book_providers/proxis.rb +0 -176
  133. data/lib/alexandria/book_providers/siciliano.rb +0 -256
  134. data/lib/alexandria/book_providers/z3950.rb +0 -408
@@ -1,239 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # This file is part of Alexandria.
4
- #
5
- # See the file README.md for authorship and licensing information.
6
-
7
- # http://en.wikipedia.org/wiki/Amazon
8
-
9
- require "hpricot"
10
- require "alexandria/book_providers/amazon_ecs_util"
11
-
12
- module Alexandria
13
- class BookProviders
14
- class AmazonProvider < GenericProvider
15
- include Logging
16
- include GetText
17
- GetText.bindtextdomain(Alexandria::TEXTDOMAIN, charset: "UTF-8")
18
-
19
- # CACHE_DIR = File.join(Alexandria::Library::DIR, '.amazon_cache')
20
-
21
- LOCALES = ["ca", "de", "fr", "jp", "uk", "us"].freeze
22
-
23
- def initialize
24
- super("Amazon", "Amazon")
25
- # prefs.add("enabled", _("Enabled"), true, [true,false])
26
- prefs.add("locale", _("Locale"), "us", AmazonProvider::LOCALES)
27
- prefs.add("dev_token", _("Access key ID"), "")
28
- prefs.add("secret_key", _("Secret access key"), "")
29
- prefs.add("associate_tag", _("Associate Tag"), "")
30
-
31
- prefs.read
32
- token = prefs.variable_named("dev_token")
33
- # kill old (shorter) tokens, or previously distributed Access Key Id (see #26250)
34
-
35
- if token
36
- token.new_value = token.value.strip if token.value != token.value.strip
37
- if (token.value.size != 20) || (token.value == "0J356Z09CN88KB743582")
38
- token.new_value = ""
39
- end
40
- end
41
-
42
- secret = prefs.variable_named("secret_key")
43
- if secret && (secret.value != secret.value.strip)
44
- secret.new_value = secret.value.strip
45
- end
46
-
47
- associate = prefs.variable_named("associate_tag") or return
48
-
49
- associate.new_value = "rubyalexa-20" if associate.value.strip.empty?
50
- if associate.value != associate.value.strip
51
- associate.new_value = associate.value.strip
52
- end
53
- end
54
-
55
- def search(criterion, type)
56
- prefs.read
57
-
58
- if prefs["secret_key"].empty?
59
- raise(Amazon::RequestError, _("Provide secret key for your Amazon AWS account."))
60
- end
61
-
62
- if (config = Alexandria::Preferences.instance.http_proxy_config)
63
- host, port, user, pass = config
64
- url = "http://"
65
- url += user + ":" + pass + "@" if user && pass
66
- url += host + ":" + port.to_s
67
- ENV["http_proxy"] = url
68
- end
69
-
70
- access_key_id = prefs["dev_token"]
71
-
72
- Amazon::Ecs.options = { aWS_access_key_id: access_key_id,
73
- associateTag: prefs["associate_tag"] }
74
- Amazon::Ecs.secret_access_key = prefs["secret_key"]
75
- # #req.cache = Amazon::Search::Cache.new(CACHE_DIR)
76
- locales = AmazonProvider::LOCALES.dup
77
- locales.delete prefs["locale"]
78
- locales.unshift prefs["locale"]
79
- locales.reverse!
80
-
81
- begin
82
- request_locale = locales.pop.intern
83
- products = []
84
- case type
85
- when SEARCH_BY_ISBN
86
- criterion = Library.canonicalise_isbn(criterion)
87
- # This isn't ideal : I'd like to do an ISBN/EAN-specific search
88
- res = Amazon::Ecs.item_search(criterion,
89
- response_group: "ItemAttributes,Images",
90
- country: request_locale)
91
-
92
- res.items.each do |item|
93
- products << item
94
- end
95
- # #req.asin_search(criterion) do |product|
96
-
97
- # Shouldn't happen.
98
- # raise TooManyResultsError if products.length > 1
99
-
100
- # I had assumed that publishers were bogusly publishing
101
- # multiple editions of a book with the same ISBN, and
102
- # Amazon was distinguishing between them. So we'll log
103
- # this case, and arbitrarily return the FIRST item
104
-
105
- # Actually, this may be due to Amazon recommending a
106
- # preferred later edition of a book, in spite of our
107
- # searching on a single ISBN it can return more than one
108
- # result with different ISBNs
109
-
110
- if products.length > 1
111
- log.warn do
112
- "ISBN search at Amazon[#{request_locale}] got #{products.length} results;" \
113
- " returning the first result only"
114
- end
115
- end
116
-
117
- when SEARCH_BY_TITLE
118
- res = Amazon::Ecs.item_search(criterion,
119
- response_group: "ItemAttributes,Images",
120
- country: request_locale)
121
-
122
- res.items.each do |item|
123
- products << item if /#{criterion}/i.match?(item.get("itemattributes/title"))
124
- end
125
- # #req.keyword_search(criterion) do |product|
126
-
127
- when SEARCH_BY_AUTHORS
128
- criterion = "author:#{criterion}"
129
- res = Amazon::Ecs.item_search(criterion,
130
- response_group: "ItemAttributes,Images",
131
- country: request_locale, type: "Power")
132
- res.items.each do |item|
133
- products << item
134
- end
135
- # #req.author_search(criterion) do |product|
136
-
137
- when SEARCH_BY_KEYWORD
138
- res = Amazon::Ecs.item_search(criterion,
139
- response_group: "ItemAttributes,Images",
140
- country: request_locale)
141
-
142
- res.items.each do |item|
143
- products << item
144
- end
145
-
146
- else
147
- raise InvalidSearchTypeError
148
- end
149
- raise Amazon::RequestError, _("No products") if products.empty?
150
- # raise NoResultsError if products.empty?
151
- rescue Amazon::RequestError => ex
152
- log.debug { "Got Amazon::RequestError at #{request_locale}: #{ex}" }
153
- retry unless locales.empty?
154
- raise NoResultsError
155
- end
156
-
157
- results = []
158
- products.each do |item|
159
- next unless item.get("itemattributes/productgroup") == "Book"
160
-
161
- atts = item.search_and_convert("itemattributes")
162
- title = normalize(atts.get("title"))
163
-
164
- media = normalize(atts.get("binding"))
165
- media = nil if media == "Unknown Binding"
166
-
167
- isbn = normalize(atts.get("isbn"))
168
- isbn = (Library.canonicalise_ean(isbn) if isbn && Library.valid_isbn?(isbn))
169
- # hack, extract year by regexp (not Y10K compatible :-)
170
- /([1-9][0-9]{3})/ =~ atts.get("publicationdate")
171
- publishing_year = Regexp.last_match[1] ? Regexp.last_match[1].to_i : nil
172
- book = Book.new(title,
173
- atts.get_array("author").map { |x| normalize(x) },
174
- isbn,
175
- normalize(atts.get("manufacturer")),
176
- publishing_year,
177
- media)
178
-
179
- image_url = item.get("mediumimage/url")
180
- log.info { "Found at Amazon[#{request_locale}]: #{book.title}" }
181
- results << [book, image_url]
182
- end
183
- if type == SEARCH_BY_ISBN
184
- if results.size == 1
185
- results.first
186
- else
187
- exact_match_or_first(criterion, results)
188
- end
189
- else
190
- results
191
- end
192
- end
193
-
194
- LOCALE_URLS = {
195
- "fr" => "http://www.amazon.fr/exec/obidos/ASIN/%s",
196
- "uk" => "http://www.amazon.co.uk/exec/obidos/ASIN/%s",
197
- "de" => "http://www.amazon.de/exec/obidos/ASIN/%s",
198
- "ca" => "http://www.amazon.ca/exec/obidos/ASIN/%s",
199
- "jp" => "http://www.amazon.jp/exec/obidos/ASIN/%s",
200
- "us" => "http://www.amazon.com/exec/obidos/ASIN/%s"
201
- }.freeze
202
-
203
- def url(book)
204
- isbn = Library.canonicalise_isbn(book.isbn)
205
- url = LOCALE_URLS.fetch(prefs["locale"])
206
- url % isbn
207
- rescue StandardError => ex
208
- log.warn { "Cannot create url for book #{book}; #{ex.message}" }
209
- nil
210
- end
211
-
212
- def normalize(str)
213
- str = str.squeeze(" ").strip unless str.nil?
214
- str
215
- end
216
-
217
- private
218
-
219
- def exact_match_or_first(criterion, results)
220
- log.info { "Found multiple results for lookup: checking for exact isbn match" }
221
- query_isbn_canon = Library.canonicalise_ean(criterion)
222
- exact_match = results.find do |book, _|
223
- book_isbn_canon = Library.canonicalise_ean(book.isbn)
224
- query_isbn_canon == book_isbn_canon
225
- end
226
-
227
- if exact_match
228
- # gone through all and no ISBN match, so just return first result
229
- log.info do
230
- "no more results to check. Returning first result, just an approximation"
231
- end
232
- exact_match
233
- else
234
- results.first
235
- end
236
- end
237
- end
238
- end
239
- end
@@ -1,373 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # This file is part of Alexandria.
4
- #
5
- # See the file README.md for authorship and licensing information.
6
-
7
- require "net/http"
8
- require "hpricot"
9
- require "cgi"
10
-
11
- require "digest/sha2"
12
-
13
- module Alexandria
14
- module Amazon
15
- class RequestError < StandardError; end
16
-
17
- class Ecs
18
- include Logging
19
-
20
- SERVICE_URLS = {
21
- us: "http://webservices.amazon.com/onca/xml?Service=AWSECommerceService",
22
- uk: "http://webservices.amazon.co.uk/onca/xml?Service=AWSECommerceService",
23
- ca: "http://webservices.amazon.ca/onca/xml?Service=AWSECommerceService",
24
- de: "http://webservices.amazon.de/onca/xml?Service=AWSECommerceService",
25
- jp: "http://webservices.amazon.co.jp/onca/xml?Service=AWSECommerceService",
26
- fr: "http://webservices.amazon.fr/onca/xml?Service=AWSECommerceService"
27
- }.freeze
28
-
29
- @@options = {}
30
- @@debug = false
31
-
32
- @@secret_access_key = ""
33
-
34
- # Default search options
35
- def self.options
36
- @@options
37
- end
38
-
39
- def self.secret_access_key=(key)
40
- @@secret_access_key = key
41
- end
42
-
43
- # Set default search options
44
- def self.options=(opts)
45
- @@options = opts
46
- end
47
-
48
- # Get debug flag.
49
- def self.debug
50
- @@debug
51
- end
52
-
53
- # Set debug flag to true or false.
54
- def self.debug=(dbg)
55
- @@debug = dbg
56
- end
57
-
58
- def self.configure(&_proc)
59
- yield @@options
60
- end
61
-
62
- # Search amazon items with search terms. Default search index option is 'Books'.
63
- # For other search type other than keywords, please specify
64
- # :type => [search type param name].
65
- def self.item_search(terms, opts = {})
66
- opts[:operation] = "ItemSearch"
67
- opts[:search_index] = opts[:search_index] || "Books"
68
-
69
- type = opts.delete(:type)
70
- if type
71
- opts[type.to_sym] = terms
72
- else
73
- opts[:keywords] = terms
74
- end
75
-
76
- send_request(opts)
77
- end
78
-
79
- # Search an item by ASIN no.
80
- def self.item_lookup(item_id, opts = {})
81
- opts[:operation] = "ItemLookup"
82
- opts[:item_id] = item_id
83
-
84
- send_request(opts)
85
- end
86
-
87
- # HACK : copied from book_providers.rb
88
- def self.transport
89
- config = Alexandria::Preferences.instance.http_proxy_config
90
- config ? Net::HTTP.Proxy(*config) : Net::HTTP
91
- end
92
-
93
- # Generic send request to ECS REST service. You have to specify the
94
- # :operation parameter.
95
- def self.send_request(opts)
96
- opts = options.merge(opts) if options
97
- request_url = prepare_url(opts)
98
- log.debug { "Request URL: #{request_url}" }
99
-
100
- res = transport.get_response(URI.parse(request_url))
101
- unless res.is_a? Net::HTTPSuccess
102
- raise Amazon::RequestError, format(_("HTTP Response: %<code>s %<message>s"),
103
- code: res.code, message: res.message)
104
- end
105
-
106
- Response.new(res.body)
107
- end
108
-
109
- # Response object returned after a REST call to Amazon service.
110
- class Response
111
- # XML input is in string format
112
- def initialize(xml)
113
- @doc = Hpricot(xml)
114
- end
115
-
116
- # Return Hpricot object.
117
- attr_reader :doc
118
-
119
- # Return true if request is valid.
120
- def is_valid_request?
121
- (@doc / "isvalid").inner_html == "True"
122
- end
123
-
124
- # Return true if response has an error.
125
- def has_error?
126
- !(error.nil? || error.empty?)
127
- end
128
-
129
- # Return error message.
130
- def error
131
- Element.get(@doc, "error/message")
132
- end
133
-
134
- # Return an array of Amazon::Element item objects.
135
- def items
136
- @items ||= (@doc / "item").map { |item| Element.new(item) }
137
- @items
138
- end
139
-
140
- # Return the first item (Amazon::Element)
141
- def first_item
142
- items.first
143
- end
144
-
145
- # Return current page no if :item_page option is when initiating the request.
146
- def item_page
147
- @item_page ||= (@doc / "itemsearchrequest/itempage").inner_html.to_i
148
- @item_page
149
- end
150
-
151
- # Return total results.
152
- def total_results
153
- @total_results ||= (@doc / "totalresults").inner_html.to_i
154
- @total_results
155
- end
156
-
157
- # Return total pages.
158
- def total_pages
159
- @total_pages ||= (@doc / "totalpages").inner_html.to_i
160
- @total_pages
161
- end
162
- end
163
-
164
- def self.prepare_url(opts)
165
- country = opts.delete(:country)
166
- country = country.nil? ? "us" : country
167
- request_url = SERVICE_URLS[country.to_sym]
168
- unless request_url
169
- raise Amazon::RequestError,
170
- format(_("Invalid country '%<country>s'"), country: country)
171
- end
172
-
173
- qs = ""
174
- opts.each do |k, v|
175
- next unless v
176
-
177
- v = v.join(",") if v.is_a? Array
178
- qs << "&#{camelize(k.to_s)}=#{CGI.escape(v.to_s)}"
179
- end
180
- url = "#{request_url}#{qs}"
181
- sign_request(url)
182
- end
183
-
184
- def self.camelize(string)
185
- string.to_s
186
- .gsub(%r{/(.?)}) { "::" + Regexp.last_match[1].upcase }
187
- .gsub(/(^|_)(.)/) { Regexp.last_match[2].upcase }
188
- end
189
-
190
- def self.hmac_sha256(message, key)
191
- block_size = 64
192
- ipad = "\x36" * block_size
193
- opad = "\x5c" * block_size
194
- if key.size > block_size
195
- d = Digest::SHA256.new
196
- key = d.digest(key)
197
- end
198
-
199
- ipad_bytes = ipad.bytes.map { |b| b }
200
- opad_bytes = opad.bytes.map { |b| b }
201
- key_bytes = key.bytes.map { |b| b }
202
- ipad_xor = ""
203
- opad_xor = ""
204
- (0..key.size - 1).each do |i|
205
- ipad_xor << (ipad_bytes[i] ^ key_bytes[i])
206
- opad_xor << (opad_bytes[i] ^ key_bytes[i])
207
- end
208
-
209
- ipad = ipad_xor + ipad[key.size..-1]
210
- opad = opad_xor + opad[key.size..-1]
211
-
212
- # inner hash
213
- d1 = Digest::SHA256.new
214
- d1.update(ipad)
215
- d1.update(message)
216
- msg_hash = d1.digest
217
-
218
- # outer hash
219
- d2 = Digest::SHA256.new
220
- d2.update(opad)
221
- d2.update(msg_hash)
222
- d2.digest
223
- end
224
-
225
- def self.sign_request(request)
226
- raise AmazonNotConfiguredError unless @@secret_access_key
227
-
228
- # Step 0 : Split apart request string
229
- url_pattern = %r{http://([^/]+)(/[^?]+)\?(.*$)}
230
- url_pattern =~ request
231
- host = Regexp.last_match[1]
232
- path = Regexp.last_match[2]
233
- param_string = Regexp.last_match[3]
234
-
235
- # Step 1: enter the timestamp
236
- t = Time.now.getutc # MUST be in UTC
237
- stamp = t.strftime("%Y-%m-%dT%H:%M:%SZ")
238
- param_string += "&Timestamp=#{stamp}"
239
-
240
- # Step 2 : URL-encode
241
- param_string = param_string.gsub(",", "%2C").gsub(":", "%3A")
242
- # NOTE : take care not to double-encode
243
-
244
- # Step 3 : Split the parameter/value pairs
245
- params = param_string.split("&")
246
-
247
- # Step 4 : Sort params
248
- params.sort!
249
-
250
- # Step 5 : Rejoin the param string
251
- canonical_param_string = params.join("&")
252
-
253
- # Steps 6 & 7: Prepend HTTP request info
254
- string_to_sign = "GET\n#{host}\n#{path}\n#{canonical_param_string}"
255
-
256
- # Step 8 : Calculate RFC 2104-compliant HMAC with SHA256 hash algorithm
257
- sig = hmac_sha256(string_to_sign, @@secret_access_key)
258
- base64_sig = [sig].pack("m").strip
259
-
260
- # Step 9 : URL-encode + and = in sig
261
- base64_sig = CGI.escape(base64_sig)
262
-
263
- # Step 10 : Add the URL encoded signature to your request
264
- "http://#{host}#{path}?#{param_string}&Signature=#{base64_sig}"
265
- end
266
- end
267
-
268
- # Internal wrapper class to provide convenient method to access Hpricot element value.
269
- class Element
270
- # Pass Hpricot::Elements object
271
- def initialize(element)
272
- @element = element
273
- end
274
-
275
- # Returns Hpricot::Elments object
276
- def elem
277
- @element
278
- end
279
-
280
- # Find Hpricot::Elements matching the given path. Example: element/"author".
281
- def /(path)
282
- elements = @element / path
283
- return nil if elements.empty?
284
-
285
- elements
286
- end
287
-
288
- # Find Hpricot::Elements matching the given path, and convert to Amazon::Element.
289
- # Returns an array Amazon::Elements if more than Hpricot::Elements size is
290
- # greater than 1.
291
- def search_and_convert(path)
292
- elements = self./(path)
293
- return unless elements
294
-
295
- elements = elements.map { |element| Element.new(element) }
296
- return elements.first if elements.size == 1
297
-
298
- elements
299
- end
300
-
301
- # Get the text value of the given path, leave empty to retrieve current element value.
302
- def get(path = "")
303
- Element.get(@element, path)
304
- end
305
-
306
- # Get the unescaped HTML text of the given path.
307
- def get_unescaped(path = "")
308
- Element.get_unescaped(@element, path)
309
- end
310
-
311
- # Get the array values of the given path.
312
- def get_array(path = "")
313
- Element.get_array(@element, path)
314
- end
315
-
316
- # Get the children element text values in hash format with the element
317
- # names as the hash keys.
318
- def get_hash(path = "")
319
- Element.get_hash(@element, path)
320
- end
321
-
322
- # Similar to #get, except an element object must be passed-in.
323
- def self.get(element, path = "")
324
- return unless element
325
-
326
- result = element.at(path)
327
- ## inner_html doesn't decode entities, hence bug #21659
328
- # result = result.inner_html if result
329
- result = result.inner_text if result
330
- result
331
- end
332
-
333
- # Similar to #get_unescaped, except an element object must be passed-in.
334
- def self.get_unescaped(element, path = "")
335
- result = get(element, path)
336
- CGI.unescapeHTML(result) if result
337
- end
338
-
339
- # Similar to #get_array, except an element object must be passed-in.
340
- def self.get_array(element, path = "")
341
- return unless element
342
-
343
- result = element / path
344
- if (result.is_a? Hpricot::Elements) || (result.is_a? Array)
345
- parsed_result = []
346
- result.each do |item|
347
- parsed_result << Element.get(item)
348
- end
349
- parsed_result
350
- else
351
- [Element.get(result)]
352
- end
353
- end
354
-
355
- # Similar to #get_hash, except an element object must be passed-in.
356
- def self.get_hash(element, path = "")
357
- result = element&.at(path)
358
- return unless result
359
-
360
- hash = {}
361
- result = result.children
362
- result.each do |item|
363
- hash[item.name.to_sym] = item.inner_html
364
- end
365
- hash
366
- end
367
-
368
- def to_s
369
- elem&.to_s
370
- end
371
- end
372
- end
373
- end