Skip to content

Commit

Permalink
Merge 30eb345 into fcb56fa
Browse files Browse the repository at this point in the history
  • Loading branch information
dukaarpad committed Jul 7, 2016
2 parents fcb56fa + 30eb345 commit 0fc127b
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 16 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,7 @@
# 2.12.0 (2016-04-29)

* Add multipart request sending option (https://www.w3.org/TR/SOAP-attachments)

# 2.11.1 (2015-05-27)

* Replace dependency on [uuid](https://rubygems.org/gems/uuid), using SecureRandom.uuid instead.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -22,7 +22,7 @@ $ gem install savon
or add it to your Gemfile like this:

```
gem 'savon', '~> 2.11.0'
gem 'savon', '~> 2.12.0'
```

## Usage example
Expand Down
42 changes: 41 additions & 1 deletion lib/savon/builder.rb
Expand Up @@ -6,6 +6,7 @@

module Savon
class Builder
attr_reader :multipart

SCHEMA_TYPES = {
"xmlns:xsd" => "http://www.w3.org/2001/XMLSchema",
Expand Down Expand Up @@ -64,7 +65,46 @@ def build_document
xml = @signature.document
end

xml
# if there are attachments for the request, we should build a multipart message according to
# https://www.w3.org/TR/SOAP-attachments
if @locals[:attachments]
message = Mail.new
xml_part = Mail::Part.new do
content_type 'text/xml'
body xml
# in Content-Type the start parameter is recommended (RFC 2387)
content_id '<soap-request-body@soap>'
end
message.add_part xml_part

if @locals[:attachments].is_a? Hash
@locals[:attachments].each do |content_location, file|
message.add_file file.clone
message.parts.last.content_location = content_location.to_s
message.parts.last.content_id = message.parts.last.content_location
end
elsif @locals[:attachments].is_a? Array
@locals[:attachments].each do |file|
message.add_file file.clone
message.parts.last.content_location = file.is_a?(String) ? File.basename(file) : file[:filename]
message.parts.last.content_id = message.parts.last.content_location
end
end
message.ready_to_send!

# 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
message.body.set_sort_order [ "text/xml" ]

#request.headers["Content-Type"] = "multipart/related; boundary=\"#{message.body.boundary}\"; type=\"text/xml\"; start=\"#{xml_part.content_id}\""
@multipart = {
multipart_boundary: message.body.boundary,
start: xml_part.content_id,
}
message.body.encoded(message.content_transfer_encoding)
else
xml
end
end

def header_attributes
Expand Down
20 changes: 8 additions & 12 deletions lib/savon/operation.rb
Expand Up @@ -5,6 +5,7 @@
require "savon/response"
require "savon/request_logger"
require "savon/http_error"
require "mail"

module Savon
class Operation
Expand Down Expand Up @@ -66,21 +67,11 @@ def request(locals = {}, &block)
private

def create_response(response)
if multipart_supported?
Multipart::Response.new(response, @globals, @locals)
else
Response.new(response, @globals, @locals)
end
Response.new(response, @globals, @locals)
end

def multipart_supported?
return false unless @globals[:multipart] || @locals[:multipart]

if Savon.const_defined? :Multipart
true
else
raise 'Unable to find Savon::Multipart. Make sure the savon-multipart gem is installed and loaded.'
end
@globals[:multipart] || @locals[:multipart]
end

def set_locals(locals, block)
Expand Down Expand Up @@ -111,6 +102,11 @@ def build_request(builder)
# was not specified manually? [dh, 2013-01-04]
request.headers["Content-Length"] = request.body.bytesize.to_s

if builder.multipart
request.headers["Content-Type"] = "multipart/related; boundary=\"#{builder.multipart[:multipart_boundary]}\"; " +
"type=\"text/xml\"; start=\"#{builder.multipart[:start]}\""
end

request
end

Expand Down
34 changes: 34 additions & 0 deletions lib/savon/options.rb
Expand Up @@ -383,6 +383,40 @@ def attributes(attributes)
@options[:attributes] = attributes
end

# Attachments for the SOAP message (https://www.w3.org/TR/SOAP-attachments)
#
# should pass an Array or a Hash; items should be path strings or
# { filename: 'file.name', content: 'content' } objects
# The Content-ID in multipart message sections will be the filename or the key if Hash is given
#
# usage examples:
#
# response = client.call :operation1 do
# message param1: 'value'
# attachments [
# { filename: 'x1.xml', content: '<xml>abc</xml>'},
# { filename: 'x2.xml', content: '<xml>abc</xml>'}
# ]
# end
# # Content-ID will be x1.xml and x2.xml
#
# response = client.call :operation1 do
# message param1: 'value'
# attachments 'x1.xml' => '/tmp/1281ab7d7d.xml', 'x2.xml' => '/tmp/4c5v8e833a.xml'
# end
# # Content-ID will be x1.xml and x2.xml
#
# response = client.call :operation1 do
# message param1: 'value'
# attachments [ '/tmp/1281ab7d7d.xml', '/tmp/4c5v8e833a.xml']
# end
# # Content-ID will be 1281ab7d7d.xml and 4c5v8e833a.xml
#
# The Content-ID is important if you want to refer to the attachments from the SOAP request
def attachments(attachments)
@options[:attachments] = attachments
end

# Value of the SOAPAction HTTP header.
def soap_action(soap_action)
@options[:soap_action] = soap_action
Expand Down
46 changes: 45 additions & 1 deletion lib/savon/response.rb
Expand Up @@ -4,11 +4,15 @@

module Savon
class Response
include Mail::Patterns

def initialize(http, globals, locals)
@http = http
@globals = globals
@locals = locals
@attachments = []
@xml = ''
@has_parsed_body = false

build_soap_and_http_errors!
raise_soap_and_http_errors! if @globals[:raise_errors]
Expand Down Expand Up @@ -53,7 +57,12 @@ def hash
end

def xml
@http.body
if multipart?
parse_body unless @has_parsed_body
@xml
else
@http.body
end
end

alias_method :to_xml, :xml
Expand All @@ -74,8 +83,43 @@ def find(*path)
nori.find(envelope, *path)
end

def attachments
if multipart?
parse_body unless @has_parsed_body
@attachments
else
[]
end
end

private

def multipart?
!(http.headers['content-type'] =~ /^multipart/im).nil?
end

def boundary
return unless multipart?
Mail::Field.new('content-type', http.headers['content-type']).parameters['boundary']
end

def parse_body
parts = http.body.split(/(?:\A|\r\n)--#{Regexp.escape(boundary)}(?=(?:--)?\s*$)/)
parts[1..-1].to_a.each_with_index do |part, index|
header_part, body_part = part.lstrip.split(/#{CRLF}#{CRLF}|#{CRLF}#{WSP}*#{CRLF}(?!#{WSP})/m, 2)
section = Mail::Part.new(
body: body_part
)
section.header = header_part
if index == 0
@xml = section.body.to_s
else
@attachments << section
end
end
@has_parsed_body = true
end

def build_soap_and_http_errors!
@soap_fault = SOAPFault.new(@http, nori, xml) if soap_fault?
@http_error = HTTPError.new(@http) if http_error?
Expand Down
2 changes: 1 addition & 1 deletion lib/savon/version.rb
@@ -1,3 +1,3 @@
module Savon
VERSION = '2.11.1'
VERSION = '2.12.0'
end
1 change: 1 addition & 0 deletions savon.gemspec
Expand Up @@ -24,6 +24,7 @@ Gem::Specification.new do |s|
s.add_dependency "gyoku", "~> 1.2"
s.add_dependency "builder", ">= 2.1.2"
s.add_dependency "nokogiri", ">= 1.4.0"
s.add_dependency "mail", "~> 2.5"

s.add_development_dependency "rack"
s.add_development_dependency "puma", "2.0.0.b4"
Expand Down
46 changes: 46 additions & 0 deletions spec/savon/multipart_request_spec.rb
@@ -0,0 +1,46 @@
require "spec_helper"

describe Savon::Builder do

let(:globals) { Savon::GlobalOptions.new({ :endpoint => "http://example.co", :namespace => "http://v1.example.com" }) }
let(:no_wsdl) { Wasabi::Document.new }

it "building multipart request from inline content" do
locals = {
attachments: [
{ filename: 'x1.xml', content: '<xml>abc1</xml>'},
{ filename: 'x2.xml', content: '<xml>abc2</xml>'},
]
}
builder = Savon::Builder.new(:operation1, no_wsdl, globals, Savon::LocalOptions.new(locals))
request_body = builder.to_s

expect(request_body).to include('Content-Type')
expect(request_body).to match(/<[a-z]+:operation1>/)

locals[:attachments].each do |attachment|
expect(request_body).to match(/^Content-Location: #{attachment[:filename]}\s$/)
expect(request_body).to include(Base64.encode64(attachment[:content]).strip)
end

end

it "building multipart request from file" do
locals = {
attachments: {
'file.gz' => File.expand_path("../../fixtures/gzip/message.gz", __FILE__)
}
}
builder = Savon::Builder.new(:operation1, no_wsdl, globals, Savon::LocalOptions.new(locals))
request_body = builder.to_s

expect(request_body).to include('Content-Type')
expect(request_body).to match(/<[a-z]+:operation1>/)

locals[:attachments].each do |id, file|
expect(request_body).to match(/^Content-Location: #{id}\s$/)
expect(request_body.gsub("\r", "")).to include(Base64.encode64(File.read(file)).strip)
end

end
end

0 comments on commit 0fc127b

Please sign in to comment.