savon 2.11.2 → 2.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +103 -73
  4. data/CONTRIBUTING.md +15 -19
  5. data/Gemfile +2 -7
  6. data/README.md +26 -15
  7. data/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_asc.png +0 -0
  8. data/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_asc_disabled.png +0 -0
  9. data/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_both.png +0 -0
  10. data/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_desc.png +0 -0
  11. data/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_desc_disabled.png +0 -0
  12. data/coverage/assets/0.12.3/application.css +1 -0
  13. data/coverage/assets/0.12.3/application.js +7 -0
  14. data/coverage/assets/0.12.3/colorbox/border.png +0 -0
  15. data/coverage/assets/0.12.3/colorbox/controls.png +0 -0
  16. data/coverage/assets/0.12.3/colorbox/loading.gif +0 -0
  17. data/coverage/assets/0.12.3/colorbox/loading_background.png +0 -0
  18. data/coverage/assets/0.12.3/favicon_green.png +0 -0
  19. data/coverage/assets/0.12.3/favicon_red.png +0 -0
  20. data/coverage/assets/0.12.3/favicon_yellow.png +0 -0
  21. data/coverage/assets/0.12.3/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  22. data/coverage/assets/0.12.3/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  23. data/coverage/assets/0.12.3/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  24. data/coverage/assets/0.12.3/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  25. data/coverage/assets/0.12.3/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  26. data/coverage/assets/0.12.3/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  27. data/coverage/assets/0.12.3/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  28. data/coverage/assets/0.12.3/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  29. data/coverage/assets/0.12.3/images/ui-icons_222222_256x240.png +0 -0
  30. data/coverage/assets/0.12.3/images/ui-icons_2e83ff_256x240.png +0 -0
  31. data/coverage/assets/0.12.3/images/ui-icons_454545_256x240.png +0 -0
  32. data/coverage/assets/0.12.3/images/ui-icons_888888_256x240.png +0 -0
  33. data/coverage/assets/0.12.3/images/ui-icons_cd0a0a_256x240.png +0 -0
  34. data/coverage/assets/0.12.3/loading.gif +0 -0
  35. data/coverage/assets/0.12.3/magnify.png +0 -0
  36. data/coverage/index.html +21518 -0
  37. data/lib/savon/block_interface.rb +1 -0
  38. data/lib/savon/builder.rb +95 -29
  39. data/lib/savon/client.rb +1 -0
  40. data/lib/savon/core_ext/string.rb +1 -0
  41. data/lib/savon/header.rb +2 -6
  42. data/lib/savon/http_error.rb +4 -4
  43. data/lib/savon/log_message.rb +1 -0
  44. data/lib/savon/message.rb +1 -0
  45. data/lib/savon/mock/expectation.rb +1 -0
  46. data/lib/savon/mock/spec_helper.rb +1 -0
  47. data/lib/savon/mock.rb +1 -0
  48. data/lib/savon/model.rb +1 -0
  49. data/lib/savon/operation.rb +20 -18
  50. data/lib/savon/options.rb +70 -1
  51. data/lib/savon/qualified_message.rb +5 -4
  52. data/lib/savon/request.rb +18 -3
  53. data/lib/savon/request_logger.rb +8 -2
  54. data/lib/savon/response.rb +49 -2
  55. data/lib/savon/soap_fault.rb +2 -3
  56. data/lib/savon/version.rb +2 -1
  57. data/lib/savon.rb +1 -0
  58. data/savon.gemspec +10 -9
  59. data/spec/fixtures/response/empty_soap_fault.xml +13 -0
  60. data/spec/fixtures/response/no_body.xml +1 -0
  61. data/spec/fixtures/wsdl/elements_in_types.xml +43 -0
  62. data/spec/integration/support/application.rb +34 -2
  63. data/spec/integration/support/server.rb +1 -0
  64. data/spec/integration/zipcode_example_spec.rb +5 -8
  65. data/spec/savon/builder_spec.rb +2 -1
  66. data/spec/savon/client_spec.rb +5 -4
  67. data/spec/savon/core_ext/string_spec.rb +2 -1
  68. data/spec/savon/features/message_tag_spec.rb +2 -1
  69. data/spec/savon/http_error_spec.rb +9 -1
  70. data/spec/savon/log_message_spec.rb +2 -1
  71. data/spec/savon/message_spec.rb +2 -11
  72. data/spec/savon/mock_spec.rb +2 -1
  73. data/spec/savon/model_spec.rb +2 -1
  74. data/spec/savon/multipart_request_spec.rb +46 -0
  75. data/spec/savon/observers_spec.rb +2 -1
  76. data/spec/savon/operation_spec.rb +20 -43
  77. data/spec/savon/options_spec.rb +84 -5
  78. data/spec/savon/qualified_message_spec.rb +35 -1
  79. data/spec/savon/request_logger_spec.rb +2 -1
  80. data/spec/savon/request_spec.rb +99 -14
  81. data/spec/savon/response_spec.rb +7 -1
  82. data/spec/savon/soap_fault_spec.rb +12 -1
  83. data/spec/savon/softlayer_spec.rb +3 -2
  84. data/spec/spec_helper.rb +5 -4
  85. data/spec/support/adapters.rb +1 -0
  86. data/spec/support/endpoint.rb +1 -0
  87. data/spec/support/fixture.rb +1 -0
  88. data/spec/support/integration.rb +2 -1
  89. data/spec/support/stdout.rb +1 -0
  90. metadata +86 -33
  91. data/.travis.yml +0 -19
  92. data/donate.png +0 -0
  93. data/spec/integration/centra_spec.rb +0 -66
  94. data/spec/integration/email_example_spec.rb +0 -32
  95. data/spec/integration/random_quote_spec.rb +0 -23
  96. data/spec/integration/ratp_example_spec.rb +0 -28
  97. data/spec/integration/stockquote_example_spec.rb +0 -34
  98. data/spec/integration/temperature_example_spec.rb +0 -46
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Savon
2
3
  class BlockInterface
3
4
 
data/lib/savon/builder.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "savon/header"
2
3
  require "savon/message"
3
4
  require "nokogiri"
@@ -6,6 +7,7 @@ require "gyoku"
6
7
 
7
8
  module Savon
8
9
  class Builder
10
+ attr_reader :multipart
9
11
 
10
12
  SCHEMA_TYPES = {
11
13
  "xmlns:xsd" => "http://www.w3.org/2001/XMLSchema",
@@ -36,14 +38,7 @@ module Savon
36
38
  end
37
39
 
38
40
  def build_document
39
- xml_result = tag(builder, :Envelope, namespaces_with_globals) do |xml|
40
- tag(xml, :Header, header_attributes) { xml << header.to_s } unless header.empty?
41
- if @globals[:no_message_tag]
42
- tag(xml, :Body, body_attributes) { xml << message.to_s }
43
- else
44
- tag(xml, :Body, body_attributes) { xml.tag!(*namespaced_message_tag) { xml << body_message } }
45
- end
46
- end
41
+ xml_result = build_xml
47
42
 
48
43
  # if we have a signature sign the document
49
44
  if @signature
@@ -51,20 +46,19 @@ module Savon
51
46
 
52
47
  2.times do
53
48
  @header = nil
54
- @signature.document = tag(builder, :Envelope, namespaces_with_globals) do |xml|
55
- tag(xml, :Header, header_attributes) { xml << header.to_s } unless header.empty?
56
- if @globals[:no_message_tag]
57
- tag(xml, :Body, body_attributes) { xml << message.to_s }
58
- else
59
- tag(xml, :Body, body_attributes) { xml.tag!(*namespaced_message_tag) { xml << message.to_s } }
60
- end
61
- end
49
+ @signature.document = build_xml
62
50
  end
63
51
 
64
52
  xml_result = @signature.document
65
53
  end
66
54
 
67
- xml_result
55
+ # if there are attachments for the request, we should build a multipart message according to
56
+ # https://www.w3.org/TR/SOAP-attachments
57
+ if @locals[:attachments]
58
+ build_multipart_message(xml_result)
59
+ else
60
+ xml_result
61
+ end
68
62
  end
69
63
 
70
64
  def header_attributes
@@ -117,15 +111,21 @@ module Savon
117
111
  @namespaces ||= begin
118
112
  namespaces = SCHEMA_TYPES.dup
119
113
 
120
- if namespace_identifier == nil
121
- namespaces["xmlns"] = @globals[:namespace] || @wsdl.namespace
122
- else
123
- namespaces["xmlns:#{namespace_identifier}"] = @globals[:namespace] || @wsdl.namespace
124
- end
114
+ # check namespace_identifier
115
+ namespaces["xmlns#{namespace_identifier.nil? ? '' : ":#{namespace_identifier}"}"] =
116
+ @globals[:namespace] || @wsdl.namespace
125
117
 
126
- key = ["xmlns"]
127
- key << env_namespace if env_namespace && env_namespace != ""
128
- namespaces[key.join(":")] = SOAP_NAMESPACE[@globals[:soap_version]]
118
+ # check env_namespace
119
+ namespaces["xmlns#{env_namespace && env_namespace != "" ? ":#{env_namespace}" : ''}"] =
120
+ SOAP_NAMESPACE[@globals[:soap_version]]
121
+
122
+ if @wsdl&.document
123
+ @wsdl.parser.namespaces.each do |identifier, path|
124
+ next if namespaces.key?("xmlns:#{identifier}")
125
+
126
+ namespaces["xmlns:#{identifier}"] = path
127
+ end
128
+ end
129
129
 
130
130
  namespaces
131
131
  end
@@ -162,18 +162,20 @@ module Savon
162
162
  break if @locals[:message].nil?
163
163
  message_locals = @locals[:message][message.snakecase.to_sym]
164
164
  message_content = Message.new(message_tag, namespace_identifier, @types, @used_namespaces, message_locals, :unqualified, @globals[:convert_request_keys_to], @globals[:unwrap]).to_s
165
- messages << "<#{message} xsi:type=\"#{type.join(':')}\">#{message_content}</#{message}>"
165
+ messages += "<#{message} xsi:type=\"#{type.join(':')}\">#{message_content}</#{message}>"
166
166
  end
167
167
  messages
168
168
  end
169
169
 
170
170
  def message_tag
171
- message_tag = @wsdl.soap_input(@operation_name.to_sym).keys.first if @wsdl.document? and @wsdl.soap_input(@operation_name.to_sym).is_a?(Hash)
171
+ wsdl_tag_name = @wsdl.document? && @wsdl.soap_input(@operation_name.to_sym)
172
+
173
+ message_tag = wsdl_tag_name.keys.first if wsdl_tag_name.is_a?(Hash)
172
174
  message_tag ||= @locals[:message_tag]
173
- message_tag ||= @wsdl.soap_input(@operation_name.to_sym) if @wsdl.document?
175
+ message_tag ||= wsdl_tag_name
174
176
  message_tag ||= Gyoku.xml_tag(@operation_name, :key_converter => @globals[:convert_request_keys_to])
175
177
 
176
- @message_tag = message_tag.to_sym
178
+ message_tag.to_sym
177
179
  end
178
180
 
179
181
  def message_attributes
@@ -227,5 +229,69 @@ module Savon
227
229
  end
228
230
  end
229
231
 
232
+ def build_xml
233
+ tag(builder, :Envelope, namespaces_with_globals) do |xml|
234
+ tag(xml, :Header, header_attributes) { xml << header.to_s } unless header.empty?
235
+ tag(xml, :Body, body_attributes) do
236
+ if @globals[:no_message_tag]
237
+ xml << message.to_s
238
+ else
239
+ xml.tag!(*namespaced_message_tag) { xml << body_message }
240
+ end
241
+ end
242
+ end
243
+ end
244
+
245
+ def build_multipart_message(message_xml)
246
+ multipart_message = init_multipart_message(message_xml)
247
+ add_attachments_to_multipart_message(multipart_message)
248
+
249
+ multipart_message.ready_to_send!
250
+
251
+ # the mail.body.encoded algorithm reorders the parts, default order is [ "text/plain", "text/enriched", "text/html" ]
252
+ # should redefine the sort order, because the soap request xml should be the first
253
+ multipart_message.body.set_sort_order [ "text/xml" ]
254
+
255
+ multipart_message.body.encoded(multipart_message.content_transfer_encoding)
256
+ end
257
+
258
+ def init_multipart_message(message_xml)
259
+ multipart_message = Mail.new
260
+ xml_part = Mail::Part.new do
261
+ content_type 'text/xml'
262
+ body message_xml
263
+ # in Content-Type the start parameter is recommended (RFC 2387)
264
+ content_id '<soap-request-body@soap>'
265
+ end
266
+ multipart_message.add_part xml_part
267
+
268
+ #request.headers["Content-Type"] = "multipart/related; boundary=\"#{multipart_message.body.boundary}\"; type=\"text/xml\"; start=\"#{xml_part.content_id}\""
269
+ @multipart = {
270
+ multipart_boundary: multipart_message.body.boundary,
271
+ start: xml_part.content_id,
272
+ }
273
+
274
+ multipart_message
275
+ end
276
+
277
+ def add_attachments_to_multipart_message(multipart_message)
278
+ if @locals[:attachments].is_a? Hash
279
+ # hash example: { 'att1' => '/path/to/att1', 'att2' => '/path/to/att2' }
280
+ @locals[:attachments].each do |identifier, attachment|
281
+ add_attachment_to_multipart_message(multipart_message, attachment, identifier)
282
+ end
283
+ elsif @locals[:attachments].is_a? Array
284
+ # array example: [ '/path/to/att1', '/path/to/att2' ]
285
+ # array example: [ { filename: 'att1.xml', content: '<x/>' }, { filename: 'att2.xml', content: '<y/>' } ]
286
+ @locals[:attachments].each do |attachment|
287
+ add_attachment_to_multipart_message(multipart_message, attachment, attachment.is_a?(String) ? File.basename(attachment) : attachment[:filename])
288
+ end
289
+ end
290
+ end
291
+
292
+ def add_attachment_to_multipart_message(multipart_message, attachment, identifier)
293
+ multipart_message.add_file attachment.clone
294
+ multipart_message.parts.last.content_id = multipart_message.parts.last.content_location = identifier.to_s
295
+ end
230
296
  end
231
297
  end
data/lib/savon/client.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "savon/operation"
2
3
  require "savon/request"
3
4
  require "savon/options"
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module Savon
3
4
  module CoreExt
data/lib/savon/header.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "akami"
2
3
  require "gyoku"
3
4
  require "securerandom"
@@ -61,12 +62,7 @@ module Savon
61
62
  convert_to_xml({
62
63
  'wsa:Action' => @locals[:soap_action],
63
64
  'wsa:To' => @globals[:endpoint],
64
- 'wsa:MessageID' => "urn:uuid:#{SecureRandom.uuid}",
65
- attributes!: {
66
- 'wsa:MessageID' => {
67
- "xmlns:wsa" => "http://schemas.xmlsoap.org/ws/2004/08/addressing"
68
- }
69
- }
65
+ 'wsa:MessageID' => "urn:uuid:#{SecureRandom.uuid}"
70
66
  })
71
67
  end
72
68
 
@@ -1,4 +1,4 @@
1
- require "savon"
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Savon
4
4
  class HTTPError < Error
@@ -14,9 +14,9 @@ module Savon
14
14
  attr_reader :http
15
15
 
16
16
  def to_s
17
- message = "HTTP error (#{@http.code})"
18
- message << ": #{@http.body}" unless @http.body.empty?
19
- message
17
+ String.new("HTTP error (#{@http.code})").tap do |str_error|
18
+ str_error << ": #{@http.body}" unless @http.body.empty?
19
+ end
20
20
  end
21
21
 
22
22
  def to_hash
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "nokogiri"
2
3
 
3
4
  module Savon
data/lib/savon/message.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "savon/qualified_message"
2
3
  require "gyoku"
3
4
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "httpi"
2
3
 
3
4
  module Savon
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "savon/mock"
2
3
 
3
4
  module Savon
data/lib/savon/mock.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Savon
2
3
  class ExpectationError < StandardError; end
3
4
  end
data/lib/savon/model.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Savon
2
3
  module Model
3
4
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "savon/options"
2
3
  require "savon/block_interface"
3
4
  require "savon/request"
@@ -5,10 +6,16 @@ require "savon/builder"
5
6
  require "savon/response"
6
7
  require "savon/request_logger"
7
8
  require "savon/http_error"
9
+ require "mail"
8
10
 
9
11
  module Savon
10
12
  class Operation
11
13
 
14
+ SOAP_REQUEST_TYPE = {
15
+ 1 => "text/xml",
16
+ 2 => "application/soap+xml"
17
+ }
18
+
12
19
  def self.create(operation_name, wsdl, globals)
13
20
  if wsdl.document?
14
21
  ensure_name_is_symbol! operation_name
@@ -66,21 +73,7 @@ module Savon
66
73
  private
67
74
 
68
75
  def create_response(response)
69
- if multipart_supported?
70
- Multipart::Response.new(response, @globals, @locals)
71
- else
72
- Response.new(response, @globals, @locals)
73
- end
74
- end
75
-
76
- def multipart_supported?
77
- return false unless @globals[:multipart] || @locals[:multipart]
78
-
79
- if Savon.const_defined? :Multipart
80
- true
81
- else
82
- raise 'Unable to find Savon::Multipart. Make sure the savon-multipart gem is installed and loaded.'
83
- end
76
+ Response.new(response, @globals, @locals)
84
77
  end
85
78
 
86
79
  def set_locals(locals, block)
@@ -107,6 +100,15 @@ module Savon
107
100
  request.url = endpoint
108
101
  request.body = builder.to_s
109
102
 
103
+ if builder.multipart
104
+ request.gzip
105
+ request.headers["Content-Type"] = ["multipart/related",
106
+ "type=\"#{SOAP_REQUEST_TYPE[@globals[:soap_version]]}\"",
107
+ "start=\"#{builder.multipart[:start]}\"",
108
+ "boundary=\"#{builder.multipart[:multipart_boundary]}\""].join("; ")
109
+ request.headers["MIME-Version"] = "1.0"
110
+ end
111
+
110
112
  # TODO: could HTTPI do this automatically in case the header
111
113
  # was not specified manually? [dh, 2013-01-04]
112
114
  request.headers["Content-Length"] = request.body.bytesize.to_s
@@ -119,11 +121,11 @@ module Savon
119
121
  return if @locals.include?(:soap_action) && !@locals[:soap_action]
120
122
 
121
123
  # get the soap_action from local options
122
- soap_action = @locals[:soap_action]
124
+ @locals[:soap_action] ||
123
125
  # with no local option, but a wsdl, ask it for the soap_action
124
- soap_action ||= @wsdl.soap_action(@name.to_sym) if @wsdl.document?
126
+ @wsdl.document? && @wsdl.soap_action(@name.to_sym) ||
125
127
  # if there is no soap_action up to this point, fallback to a simple default
126
- soap_action ||= Gyoku.xml_tag(@name, :key_converter => @globals[:convert_request_keys_to])
128
+ Gyoku.xml_tag(@name, :key_converter => @globals[:convert_request_keys_to])
127
129
  end
128
130
 
129
131
  def endpoint
data/lib/savon/options.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "logger"
2
3
  require "httpi"
3
4
 
@@ -78,6 +79,7 @@ module Savon
78
79
  :namespaces => {},
79
80
  :logger => Logger.new($stdout),
80
81
  :log => false,
82
+ :log_headers => true,
81
83
  :filters => [],
82
84
  :pretty_print_xml => false,
83
85
  :raise_errors => true,
@@ -137,7 +139,7 @@ module Savon
137
139
 
138
140
  # Proxy server to use for all requests.
139
141
  def proxy(proxy)
140
- @options[:proxy] = proxy
142
+ @options[:proxy] = proxy unless proxy.nil?
141
143
  end
142
144
 
143
145
  # A Hash of HTTP headers.
@@ -155,6 +157,11 @@ module Savon
155
157
  @options[:read_timeout] = read_timeout
156
158
  end
157
159
 
160
+ # Write timeout in seconds.
161
+ def write_timeout(write_timeout)
162
+ @options[:write_timeout] = write_timeout
163
+ end
164
+
158
165
  # The encoding to use. Defaults to "UTF-8".
159
166
  def encoding(encoding)
160
167
  @options[:encoding] = encoding
@@ -213,6 +220,11 @@ module Savon
213
220
  @options[:logger].level = levels[level]
214
221
  end
215
222
 
223
+ # To log headers or not.
224
+ def log_headers(log_headers)
225
+ @options[:log_headers] = log_headers
226
+ end
227
+
216
228
  # A list of XML tags to filter from logged SOAP messages.
217
229
  def filters(*filters)
218
230
  @options[:filters] = filters.flatten
@@ -228,6 +240,16 @@ module Savon
228
240
  @options[:ssl_version] = version
229
241
  end
230
242
 
243
+ # Specifies the SSL version to use.
244
+ def ssl_min_version(version)
245
+ @options[:ssl_min_version] = version
246
+ end
247
+
248
+ # Specifies the SSL version to use.
249
+ def ssl_max_version(version)
250
+ @options[:ssl_max_version] = version
251
+ end
252
+
231
253
  # Whether and how to to verify the connection.
232
254
  def ssl_verify_mode(verify_mode)
233
255
  @options[:ssl_verify_mode] = verify_mode
@@ -268,6 +290,19 @@ module Savon
268
290
  @options[:ssl_ca_cert] = cert
269
291
  end
270
292
 
293
+ def ssl_ciphers(ciphers)
294
+ @options[:ssl_ciphers] = ciphers
295
+ end
296
+
297
+ # Sets the ca cert path.
298
+ def ssl_ca_cert_path(path)
299
+ @options[:ssl_ca_cert_path] = path
300
+ end
301
+
302
+ # Sets the ssl cert store.
303
+ def ssl_cert_store(store)
304
+ @options[:ssl_cert_store] = store
305
+ end
271
306
 
272
307
  # HTTP basic auth credentials.
273
308
  def basic_auth(*credentials)
@@ -383,6 +418,40 @@ module Savon
383
418
  @options[:attributes] = attributes
384
419
  end
385
420
 
421
+ # Attachments for the SOAP message (https://www.w3.org/TR/SOAP-attachments)
422
+ #
423
+ # should pass an Array or a Hash; items should be path strings or
424
+ # { filename: 'file.name', content: 'content' } objects
425
+ # The Content-ID in multipart message sections will be the filename or the key if Hash is given
426
+ #
427
+ # usage examples:
428
+ #
429
+ # response = client.call :operation1 do
430
+ # message param1: 'value'
431
+ # attachments [
432
+ # { filename: 'x1.xml', content: '<xml>abc</xml>'},
433
+ # { filename: 'x2.xml', content: '<xml>abc</xml>'}
434
+ # ]
435
+ # end
436
+ # # Content-ID will be x1.xml and x2.xml
437
+ #
438
+ # response = client.call :operation1 do
439
+ # message param1: 'value'
440
+ # attachments 'x1.xml' => '/tmp/1281ab7d7d.xml', 'x2.xml' => '/tmp/4c5v8e833a.xml'
441
+ # end
442
+ # # Content-ID will be x1.xml and x2.xml
443
+ #
444
+ # response = client.call :operation1 do
445
+ # message param1: 'value'
446
+ # attachments [ '/tmp/1281ab7d7d.xml', '/tmp/4c5v8e833a.xml']
447
+ # end
448
+ # # Content-ID will be 1281ab7d7d.xml and 4c5v8e833a.xml
449
+ #
450
+ # The Content-ID is important if you want to refer to the attachments from the SOAP request
451
+ def attachments(attachments)
452
+ @options[:attachments] = attachments
453
+ end
454
+
386
455
  # Value of the SOAPAction HTTP header.
387
456
  def soap_action(soap_action)
388
457
  @options[:soap_action] = soap_action
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "gyoku"
2
3
 
3
4
  module Savon
@@ -9,7 +10,7 @@ module Savon
9
10
  end
10
11
 
11
12
  def to_hash(hash, path)
12
- return unless hash
13
+ return hash unless hash
13
14
  return hash.map { |value| to_hash(value, path) } if hash.is_a?(Array)
14
15
  return hash.to_s unless hash.is_a?(Hash)
15
16
 
@@ -26,7 +27,7 @@ module Savon
26
27
  translated_key = translate_tag(key)
27
28
  newkey = add_namespaces_to_values(key, path).first
28
29
  newpath = path + [translated_key]
29
- newhash[newkey] = to_hash(value, newpath)
30
+ newhash[newkey] = to_hash(value, @types[newpath] ? [@types[newpath]] : newpath)
30
31
  end
31
32
  end
32
33
  newhash
@@ -43,8 +44,8 @@ module Savon
43
44
  Array(values).collect do |value|
44
45
  translated_value = translate_tag(value)
45
46
  namespace_path = path + [translated_value]
46
- namespace = @used_namespaces[namespace_path]
47
- namespace.blank? ? value : "#{namespace}:#{translated_value}"
47
+ namespace = @used_namespaces[namespace_path] || ''
48
+ namespace.empty? ? value : "#{namespace}:#{translated_value}"
48
49
  end
49
50
  end
50
51
  end
data/lib/savon/request.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "httpi"
2
3
 
3
4
  module Savon
@@ -21,18 +22,25 @@ module Savon
21
22
  def configure_timeouts
22
23
  @http_request.open_timeout = @globals[:open_timeout] if @globals.include? :open_timeout
23
24
  @http_request.read_timeout = @globals[:read_timeout] if @globals.include? :read_timeout
25
+ @http_request.write_timeout = @globals[:write_timeout] if @globals.include? :write_timeout
24
26
  end
25
27
 
26
28
  def configure_ssl
27
29
  @http_request.auth.ssl.ssl_version = @globals[:ssl_version] if @globals.include? :ssl_version
30
+ @http_request.auth.ssl.min_version = @globals[:ssl_min_version] if @globals.include? :ssl_min_version
31
+ @http_request.auth.ssl.max_version = @globals[:ssl_max_version] if @globals.include? :ssl_max_version
32
+
28
33
  @http_request.auth.ssl.verify_mode = @globals[:ssl_verify_mode] if @globals.include? :ssl_verify_mode
34
+ @http_request.auth.ssl.ciphers = @globals[:ssl_ciphers] if @globals.include? :ssl_ciphers
29
35
 
30
36
  @http_request.auth.ssl.cert_key_file = @globals[:ssl_cert_key_file] if @globals.include? :ssl_cert_key_file
31
- @http_request.auth.ssl.cert_key = @globals[:ssl_cert_key] if @globals.include? :ssl_cert_key
37
+ @http_request.auth.ssl.cert_key = @globals[:ssl_cert_key] if @globals.include? :ssl_cert_key
32
38
  @http_request.auth.ssl.cert_file = @globals[:ssl_cert_file] if @globals.include? :ssl_cert_file
33
- @http_request.auth.ssl.cert = @globals[:ssl_cert] if @globals.include? :ssl_cert
39
+ @http_request.auth.ssl.cert = @globals[:ssl_cert] if @globals.include? :ssl_cert
34
40
  @http_request.auth.ssl.ca_cert_file = @globals[:ssl_ca_cert_file] if @globals.include? :ssl_ca_cert_file
35
- @http_request.auth.ssl.ca_cert = @globals[:ssl_ca_cert] if @globals.include? :ssl_ca_cert
41
+ @http_request.auth.ssl.ca_cert_path = @globals[:ssl_ca_cert_path] if @globals.include? :ssl_ca_cert_path
42
+ @http_request.auth.ssl.ca_cert = @globals[:ssl_ca_cert] if @globals.include? :ssl_ca_cert
43
+ @http_request.auth.ssl.cert_store = @globals[:ssl_cert_store] if @globals.include? :ssl_cert_store
36
44
 
37
45
  @http_request.auth.ssl.cert_key_password = @globals[:ssl_cert_key_password] if @globals.include? :ssl_cert_key_password
38
46
  end
@@ -55,12 +63,19 @@ module Savon
55
63
  def build
56
64
  configure_proxy
57
65
  configure_timeouts
66
+ configure_headers
58
67
  configure_ssl
59
68
  configure_auth
60
69
  configure_redirect_handling
61
70
 
62
71
  @http_request
63
72
  end
73
+
74
+ private
75
+
76
+ def configure_headers
77
+ @http_request.headers = @globals[:headers] if @globals.include? :headers
78
+ end
64
79
  end
65
80
 
66
81
  class SOAPRequest < HTTPRequest
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "savon/log_message"
2
3
 
3
4
  module Savon
@@ -23,21 +24,26 @@ module Savon
23
24
  @globals[:log]
24
25
  end
25
26
 
27
+ def log_headers?
28
+ @globals[:log_headers]
29
+ end
30
+
26
31
  private
27
32
 
28
33
  def log_request(request)
29
34
  logger.info { "SOAP request: #{request.url}" }
30
- logger.info { headers_to_log(request.headers) }
35
+ logger.info { headers_to_log(request.headers) } if log_headers?
31
36
  logger.debug { body_to_log(request.body) }
32
37
  end
33
38
 
34
39
  def log_response(response)
35
40
  logger.info { "SOAP response (status #{response.code})" }
41
+ logger.debug { headers_to_log(response.headers) } if log_headers?
36
42
  logger.debug { body_to_log(response.body) }
37
43
  end
38
44
 
39
45
  def headers_to_log(headers)
40
- headers.map { |key, value| "#{key}: #{value}" }.join(", ")
46
+ headers.map { |key, value| "#{key}: #{value}" }.join("\n")
41
47
  end
42
48
 
43
49
  def body_to_log(body)
@@ -1,14 +1,20 @@
1
+ # frozen_string_literal: true
1
2
  require "nori"
2
3
  require "savon/soap_fault"
3
4
  require "savon/http_error"
4
5
 
5
6
  module Savon
6
7
  class Response
8
+ CRLF = /\r\n/
9
+ WSP = /[#{%Q|\x9\x20|}]/
7
10
 
8
11
  def initialize(http, globals, locals)
9
12
  @http = http
10
13
  @globals = globals
11
14
  @locals = locals
15
+ @attachments = []
16
+ @xml = ''
17
+ @has_parsed_body = false
12
18
 
13
19
  build_soap_and_http_errors!
14
20
  raise_soap_and_http_errors! if @globals[:raise_errors]
@@ -53,7 +59,12 @@ module Savon
53
59
  end
54
60
 
55
61
  def xml
56
- @http.body
62
+ if multipart?
63
+ parse_body unless @has_parsed_body
64
+ @xml
65
+ else
66
+ @http.body
67
+ end
57
68
  end
58
69
 
59
70
  alias_method :to_xml, :xml
@@ -69,13 +80,49 @@ module Savon
69
80
 
70
81
  def find(*path)
71
82
  envelope = nori.find(hash, 'Envelope')
72
- raise_invalid_response_error! unless envelope
83
+ raise_invalid_response_error! unless envelope.is_a?(Hash)
73
84
 
74
85
  nori.find(envelope, *path)
75
86
  end
76
87
 
88
+ def attachments
89
+ if multipart?
90
+ parse_body unless @has_parsed_body
91
+ @attachments
92
+ else
93
+ []
94
+ end
95
+ end
96
+
97
+ def multipart?
98
+ !(http.headers['content-type'] =~ /^multipart/im).nil?
99
+ end
100
+
77
101
  private
78
102
 
103
+ def boundary
104
+ return unless multipart?
105
+ Mail::Field.new('content-type', http.headers['content-type']).parameters['boundary']
106
+ end
107
+
108
+ def parse_body
109
+ http.body.force_encoding Encoding::ASCII_8BIT
110
+ parts = http.body.split(/(?:\A|\r\n)--#{Regexp.escape(boundary)}(?=(?:--)?\s*$)/)
111
+ parts[1..-1].to_a.each_with_index do |part, index|
112
+ header_part, body_part = part.lstrip.split(/#{CRLF}#{CRLF}|#{CRLF}#{WSP}*#{CRLF}(?!#{WSP})/m, 2)
113
+ section = Mail::Part.new(
114
+ body: body_part
115
+ )
116
+ section.header = header_part
117
+ if index == 0
118
+ @xml = section.body.to_s
119
+ else
120
+ @attachments << section
121
+ end
122
+ end
123
+ @has_parsed_body = true
124
+ end
125
+
79
126
  def build_soap_and_http_errors!
80
127
  @soap_fault = SOAPFault.new(@http, nori, xml) if soap_fault?
81
128
  @http_error = HTTPError.new(@http) if http_error?