lib/rforce/binding.rb in rforce-0.11 vs lib/rforce/binding.rb in rforce-0.12

- old
+ new

@@ -42,103 +42,91 @@ AssignmentRuleHeaderUsingRuleId = '<partner:AssignmentRuleHeader soap:mustUnderstand="1"><partner:assignmentRuleId>%s</partner:assignmentRuleId></partner:AssignmentRuleHeader>' AssignmentRuleHeaderUsingDefaultRule = '<partner:AssignmentRuleHeader soap:mustUnderstand="1"><partner:useDefaultRule>true</partner:useDefaultRule></partner:AssignmentRuleHeader>' MruHeader = '<partner:MruHeader soap:mustUnderstand="1"><partner:updateMru>true</partner:updateMru></partner:MruHeader>' ClientIdHeader = '<partner:CallOptions soap:mustUnderstand="1"><partner:client>%s</partner:client></partner:CallOptions>' - # Connect to the server securely. If you pass an oauth hash, it - # must contain the keys :consumer_key, :consumer_secret, + # Create a binding to the server (after which you can call login + # or login_with_oauth to connect to it). If you pass an oauth + # hash, it must contain the keys :consumer_key, :consumer_secret, # :access_token, :access_secret, and :login_url. # # proxy may be a URL of the form http://user:pass@example.com:port # - def initialize(url, sid = nil, oauth = nil, proxy = nil) + # if a logger is specified, it will be used for very verbose SOAP logging + # + def initialize(url, sid = nil, oauth = nil, proxy = nil, logger = nil) @session_id = sid @oauth = oauth @proxy = proxy @batch_size = DEFAULT_BATCH_SIZE - - init_server(url) + @logger = logger + @url = URI.parse(url) end - def show_debug ENV['SHOWSOAP'] == 'true' end + def create_server(url) + server = Net::HTTP.Proxy(@proxy).new(url.host, url.port) + server.use_ssl = (url.scheme == 'https') + server.verify_mode = OpenSSL::SSL::VERIFY_NONE - def init_server(url) - @url = URI.parse(url) + # run ruby with -d or env variable SHOWSOAP=true to see SOAP wiredumps. + server.set_debug_output $stderr if show_debug - if (@oauth) - consumer = OAuth::Consumer.new \ - @oauth[:consumer_key], - @oauth[:consumer_secret], - { - :site => url, - :proxy => @proxy - } - - consumer.http.set_debug_output $stderr if show_debug - - @server = OAuth::AccessToken.new \ - consumer, - @oauth[:access_token], - @oauth[:access_secret] - - class << @server - alias_method :post2, :post - end - else - @server = Net::HTTP.Proxy(@proxy).new(@url.host, @url.port) - @server.use_ssl = @url.scheme == 'https' - @server.verify_mode = OpenSSL::SSL::VERIFY_NONE - - # run ruby with -d or env variable SHOWSOAP=true to see SOAP wiredumps. - @server.set_debug_output $stderr if show_debug - end + return server end - # Connect to remote server - # - def connect(user, password) - @user = user - @password = password - - call_remote(:login, [:username, user, :password, password]) - end - # Log in to the server with a user name and password, remembering # the session ID returned to us by Salesforce. def login(user, password) - response = connect(user, password) + @user = user + @password = password + @server = create_server(@url) + response = call_remote(:login, [:username, user, :password, password]) raise "Incorrect user name / password [#{response.Fault}]" unless response.loginResponse - result = response[:loginResponse][:result] + result = response[:loginResponse][:result] @session_id = result[:sessionId] + @url = URI.parse(result[:serverUrl]) + @server = create_server(@url) - init_server(result[:serverUrl]) - - response + return response end # Log in to the server with OAuth, remembering # the session ID returned to us by Salesforce. def login_with_oauth - result = @server.post \ - @oauth[:login_url], + consumer = OAuth::Consumer.new \ + @oauth[:consumer_key], + @oauth[:consumer_secret] + + access = OAuth::AccessToken.new \ + consumer, @oauth[:access_token], + @oauth[:access_secret] + + login_url = @oauth[:login_url] + + result = access.post \ + login_url, '', {'content-type' => 'application/x-www-form-urlencoded'} case result when Net::HTTPSuccess - doc = REXML::Document.new result.body + doc = REXML::Document.new result.body @session_id = doc.elements['*/sessionId'].text - server_url = doc.elements['*/serverUrl'].text - init_server server_url + @url = URI.parse(doc.elements['*/serverUrl'].text) + @server = access - return {:sessionId => @sessionId, :serverUrl => server_url} + class << @server + alias_method :post2, :post + end + + return {:sessionId => @session_id, :serverUrl => @url.to_s} when Net::HTTPUnauthorized raise 'Invalid OAuth tokens' else raise "Unexpected error: #{response.inspect}" end @@ -146,12 +134,15 @@ # Call a method on the remote server. Arguments can be # a hash or (if order is important) an array of alternating # keys and values. def call_remote(method, args) + # Different URI requirements for regular vs. OAuth. This is + # *screaming* for a refactor. + fallback_soap_url = @oauth ? @url.to_s : @url.path - urn, soap_url = block_given? ? yield : ["urn:partner.soap.sforce.com", @url.path] + urn, soap_url = block_given? ? yield : ["urn:partner.soap.sforce.com", fallback_soap_url] # Create XML text from the arguments. expanded = '' @builder = Builder::XmlMarkup.new(:target => expanded) expand(@builder, {method => args}, urn) @@ -179,10 +170,11 @@ end # Fill in the blanks of the SOAP envelope with our # session ID and the expanded XML of our request. request = (Envelope % [@session_id, extra_headers, expanded]) + @logger && @logger.info("RForce request: #{request}") # reset the batch size for the next request @batch_size = DEFAULT_BATCH_SIZE # gzip request @@ -199,33 +191,46 @@ headers['Accept-Encoding'] = 'gzip' headers['Content-Encoding'] = 'gzip' end # Send the request to the server and read the response. + @logger && @logger.info("RForce request to host #{@server} url #{soap_url} headers: #{headers}") response = @server.post2(soap_url, request.lstrip, headers) # decode if we have encoding content = decode(response) + # Fix charset encoding. Needed because the "content" variable may contain a UTF-8 + # or ISO-8859-1 string, but is carrying the US-ASCII encoding. + content = fix_encoding(content) + # Check to see if INVALID_SESSION_ID was raised and try to relogin in if method != :login and @session_id and content =~ /sf:INVALID_SESSION_ID/ - login(@user, @password) + if @user + login(@user, @password) + else + raise "INVALID_SESSION_ID" + end # repackage and rencode request with the new session id request = (Envelope % [@session_id, extra_headers, expanded]) request = encode(request) # Send the request to the server and read the response. response = @server.post2(soap_url, request.lstrip, headers) content = decode(response) + + # Fix charset encoding. Needed because the "content" variable may contain a UTF-8 + # or ISO-8859-1 string, but is carrying the US-ASCII encoding. + content = fix_encoding(content) end + @logger && @logger.info("RForce response: #{content}") SoapResponse.new(content).parse end - # decode gzip def decode(response) encoding = response['Content-Encoding'] # return body if no encoding @@ -244,11 +249,10 @@ else response.body end end - # encode gzip def encode(request) return request if show_debug begin @@ -259,9 +263,31 @@ ensure gzw.close end end + # fix invalid US-ASCII strings by applying the correct encoding on ruby 1.9+ + def fix_encoding(string) + if [:valid_encoding?, :force_encoding].all? { |m| string.respond_to?(m) } + if !string.valid_encoding? + # The 2 possible encodings in responses are UTF-8 and ISO-8859-1 + # http://www.salesforce.com/us/developer/docs/api/Content/implementation_considerations.htm#topic-title_international + # + ["UTF-8", "ISO-8859-1"].each do |encoding_name| + + s = string.dup.force_encoding(encoding_name) + + if s.valid_encoding? + return s + end + end + + raise "Invalid encoding in SOAP response: not in [US-ASCII, UTF-8, ISO-8859-1]" + end + end + + return string + end # Turns method calls on this object into remote SOAP calls. def method_missing(method, *args) unless args.empty? || (args.size == 1 && [Hash, Array].include?(args[0].class)) raise 'Expected at most 1 Hash or Array argument'