diff --git a/lib/savon/builder.rb b/lib/savon/builder.rb index 13a17ef6..69d6dcc1 100644 --- a/lib/savon/builder.rb +++ b/lib/savon/builder.rb @@ -38,18 +38,23 @@ def pretty end def build_document - xml_result = build_xml + # check if xml was already provided + if @locals.include? :xml + xml_result = @locals[:xml] + else + xml_result = build_xml - # if we have a signature sign the document - if @signature - @signature.document = xml_result + # if we have a signature sign the document + if @signature + @signature.document = xml_result - 2.times do - @header = nil - @signature.document = build_xml - end + 2.times do + @header = nil + @signature.document = build_xml + end - xml_result = @signature.document + xml_result = @signature.document + end end # if there are attachments for the request, we should build a multipart message according to @@ -70,7 +75,6 @@ def body_attributes end def to_s - return @locals[:xml] if @locals.include? :xml build_document end @@ -254,15 +258,28 @@ def build_multipart_message(message_xml) # the mail.body.encoded algorithm reorders the parts, default order is [ "text/plain", "text/enriched", "text/html" ] # should redefine the sort order, because the soap request xml should be the first - multipart_message.body.set_sort_order [ "text/xml" ] + multipart_message.body.set_sort_order ['application/xop+xml', 'text/xml'] multipart_message.body.encoded(multipart_message.content_transfer_encoding) end def init_multipart_message(message_xml) multipart_message = Mail.new + + # MTOM differs from general SOAP attachments: + # 1. binary encoding + # 2. application/xop+xml mime type + if @locals[:mtom] + type = "application/xop+xml; charset=#{@globals[:encoding]}; type=\"text/xml\"" + + multipart_message.transport_encoding = 'binary' + message_xml.force_encoding('BINARY') + else + type = 'text/xml' + end + xml_part = Mail::Part.new do - content_type 'text/xml' + content_type type body message_xml # in Content-Type the start parameter is recommended (RFC 2387) content_id '' diff --git a/lib/savon/operation.rb b/lib/savon/operation.rb index b28a806d..8618f8f1 100644 --- a/lib/savon/operation.rb +++ b/lib/savon/operation.rb @@ -17,6 +17,7 @@ class Operation 1 => "text/xml", 2 => "application/soap+xml" } + SOAP_REQUEST_TYPE_MTOM = "application/xop+xml" def self.create(operation_name, wsdl, globals) if wsdl.document? @@ -118,18 +119,21 @@ def build_connection(builder) :headers => @locals[:headers] ) do |connection| if builder.multipart - connection.request :gzip - connection.headers["Content-Type"] = %W[multipart/related - type="#{SOAP_REQUEST_TYPE[@globals[:soap_version]]}", - start="#{builder.multipart[:start]}", - boundary="#{builder.multipart[:multipart_boundary]}"].join("; ") + ctype_headers = ["multipart/related"] + if @locals[:mtom] + ctype_headers << "type=\"#{SOAP_REQUEST_TYPE_MTOM}\"" + ctype_headers << "start-info=\"text/xml\"" + else + ctype_headers << "type=\"#{SOAP_REQUEST_TYPE[@globals[:soap_version]]}\"" + connection.request :gzip + end + connection.headers["Content-Type"] = (ctype_headers + ["start=\"#{builder.multipart[:start]}\"", + "boundary=\"#{builder.multipart[:multipart_boundary]}\""]).join("; ") connection.headers["MIME-Version"] = "1.0" end connection.headers["Content-Length"] = @locals[:body].bytesize.to_s end - - end def soap_action diff --git a/lib/savon/options.rb b/lib/savon/options.rb index 4a3c78be..a3848b93 100644 --- a/lib/savon/options.rb +++ b/lib/savon/options.rb @@ -397,7 +397,8 @@ def initialize(options = {}) :advanced_typecasting => true, :response_parser => :nokogiri, :multipart => false, - :body => false + :body => false, + :mtom => false } super defaults.merge(options) @@ -460,6 +461,11 @@ def attachments(attachments) @options[:attachments] = attachments end + # Instruct Savon to send attachments using MTOM https://www.w3.org/TR/soap12-mtom/ + def mtom(mtom) + @options[:mtom] = mtom + end + # Value of the SOAPAction HTTP header. def soap_action(soap_action) @options[:soap_action] = soap_action @@ -489,6 +495,11 @@ def response_parser(parser) @options[:response_parser] = parser end + # Pass already configured Nori instance. + def nori(nori) + @options[:nori] = nori + end + # Instruct Savon to create a multipart response if available. def multipart(multipart) @options[:multipart] = multipart diff --git a/lib/savon/request_logger.rb b/lib/savon/request_logger.rb index e143d871..454878c9 100644 --- a/lib/savon/request_logger.rb +++ b/lib/savon/request_logger.rb @@ -50,7 +50,7 @@ def headers_to_log(headers) end def body_to_log(body) - LogMessage.new(body, @globals[:filters], @globals[:pretty_print_xml]).to_s + LogMessage.new(body, @globals[:filters], @globals[:pretty_print_xml]).to_s.force_encoding(@globals[:encoding]) end end diff --git a/lib/savon/response.rb b/lib/savon/response.rb index 9ca4a35b..e148182b 100644 --- a/lib/savon/response.rb +++ b/lib/savon/response.rb @@ -142,20 +142,16 @@ def xml_namespaces end def nori - return @nori if @nori + return @locals[:nori] if @locals[:nori] - nori_options = { - :delete_namespace_attributes => @globals[:delete_namespace_attributes], - :strip_namespaces => @globals[:strip_namespaces], - :convert_tags_to => @globals[:convert_response_tags_to], - :convert_attributes_to => @globals[:convert_attributes_to], - :advanced_typecasting => @locals[:advanced_typecasting], - :parser => @locals[:response_parser] - } - - non_nil_nori_options = nori_options.reject { |_, value| value.nil? } - @nori = Nori.new(non_nil_nori_options) + @nori ||= Nori.new({ + :delete_namespace_attributes => @globals[:delete_namespace_attributes], + :strip_namespaces => @globals[:strip_namespaces], + :convert_tags_to => @globals[:convert_response_tags_to], + :convert_attributes_to => @globals[:convert_attributes_to], + :advanced_typecasting => @locals[:advanced_typecasting], + :parser => @locals[:response_parser] + }.reject { |_, value| value.nil? }) end - end end diff --git a/spec/savon/operation_spec.rb b/spec/savon/operation_spec.rb index 24ee8cbc..a465b22d 100644 --- a/spec/savon/operation_spec.rb +++ b/spec/savon/operation_spec.rb @@ -197,6 +197,78 @@ def new_operation(operation_name, wsdl, globals) end end + describe "attachments" do + context "soap_version 1" do + it "sends requests with content-type text/xml" do + globals.endpoint @server.url(:multipart) + operation = new_operation(:example, no_wsdl, globals) + req = operation.request do + attachments [ + { filename: 'x1.xml', content: 'abc'}, + { filename: 'x2.xml', content: 'cde'}, + ] + end + expect(req.headers["Content-Type"]).to start_with "multipart/related; type=\"text/xml\"; " + end + end + context "soap_version 2" do + it "sends requests with content-type application/soap+xml" do + globals.endpoint @server.url(:multipart) + globals.soap_version 2 + operation = new_operation(:example, no_wsdl, globals) + req = operation.request do + attachments [ + { filename: 'x1.xml', content: 'abc'}, + { filename: 'x2.xml', content: 'cde'}, + ] + end + expect(req.headers["Content-Type"]).to start_with "multipart/related; type=\"application/soap+xml\"; " + end + end + context "MTOM" do + it "sends request with content-type header application/xop+xml" do + globals.endpoint @server.url(:multipart) + operation = new_operation(:example, no_wsdl, globals) + req = operation.request do + mtom true + attachments [ + { filename: 'x1.xml', content: 'abc'}, + { filename: 'x2.xml', content: 'cde'}, + ] + end + expect(req.headers["Content-Type"]).to start_with "multipart/related; type=\"application/xop+xml\"; start-info=\"text/xml\"; start=\"\"; boundary=\"--==_mimepart_" + end + + it "sends attachments with Content-Transfer-Encoding: binary" do + globals.endpoint @server.url(:multipart) + operation = new_operation(:example, no_wsdl, globals) + req = operation.request do + mtom true + attachments [ + { filename: 'x1.xml', content: 'abc'}, + { filename: 'x2.xml', content: 'cde'}, + ] + end + expect(req.body.to_s).to include("filename=x1.xml\r\nContent-Transfer-Encoding: binary") + end + + it "successfully makes request" do + globals.endpoint @server.url(:multipart) + operation = new_operation(:example, no_wsdl, globals) + response = operation.call do + mtom true + attachments [ + { filename: 'x1.xml', content: 'abc'}, + { filename: 'x2.xml', content: 'cde'}, + ] + end + + expect(response.multipart?).to be true + expect(response.attachments.first.content_id).to include('attachment1') + end + end + end + def inspect_request(response) hash = JSON.parse(response.http.body) OpenStruct.new(hash)