require 'net/https' require 'uri' require 'zlib' require 'stringio' require 'rexml/document' require 'builder' require 'oauth' module RForce # Implements the connection to the Salesforce server. class Binding include RForce # Increase the maximum fetch size to 2000, as allowed by Salesforce # Added by Raymond Gao DEFAULT_BATCH_SIZE = 2000 attr_accessor :batch_size, :url, :assignment_rule_id, :use_default_rule, :update_mru, :client_id, :trigger_user_email, :trigger_other_email, :trigger_auto_response_email # Fill in the guts of this typical SOAP envelope # with the session ID and the body of the SOAP request. Envelope = <<-HERE %s %d %s %s HERE AssignmentRuleHeaderUsingRuleId = '%s' AssignmentRuleHeaderUsingDefaultRule = 'true' MruHeader = 'true' ClientIdHeader = '%s' # Connect to the server securely. 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) @session_id = sid @oauth = oauth @proxy = proxy @batch_size = DEFAULT_BATCH_SIZE init_server(url) end def show_debug ENV['SHOWSOAP'] == 'true' end def init_server(url) @url = URI.parse(url) 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 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) raise "Incorrect user name / password [#{response.Fault}]" unless response.loginResponse result = response[:loginResponse][:result] @session_id = result[:sessionId] init_server(result[:serverUrl]) 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], '', {'content-type' => 'application/x-www-form-urlencoded'} case result when Net::HTTPSuccess doc = REXML::Document.new result.body @session_id = doc.elements['*/sessionId'].text server_url = doc.elements['*/serverUrl'].text init_server server_url return {:sessionId => @sessionId, :serverUrl => server_url} when Net::HTTPUnauthorized raise 'Invalid OAuth tokens' else raise "Unexpected error: #{response.inspect}" end end # 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) urn, soap_url = block_given? ? yield : ["urn:partner.soap.sforce.com", @url.path] # Create XML text from the arguments. expanded = '' @builder = Builder::XmlMarkup.new(:target => expanded) expand(@builder, {method => args}, urn) extra_headers = "" extra_headers << (AssignmentRuleHeaderUsingRuleId % assignment_rule_id) if assignment_rule_id extra_headers << AssignmentRuleHeaderUsingDefaultRule if use_default_rule extra_headers << MruHeader if update_mru extra_headers << (ClientIdHeader % client_id) if client_id if trigger_user_email or trigger_other_email or trigger_auto_response_email extra_headers << '' extra_headers << 'true' if trigger_user_email extra_headers << 'true' if trigger_other_email extra_headers << 'true' if trigger_auto_response_email extra_headers << '' end # Fill in the blanks of the SOAP envelope with our # session ID and the expanded XML of our request. request = (Envelope % [@session_id, @batch_size, extra_headers, expanded]) # reset the batch size for the next request @batch_size = DEFAULT_BATCH_SIZE # gzip request request = encode(request) headers = { 'Connection' => 'Keep-Alive', 'Content-Type' => 'text/xml', 'SOAPAction' => '""', 'User-Agent' => 'activesalesforce rforce/1.0' } unless show_debug headers['Accept-Encoding'] = 'gzip' headers['Content-Encoding'] = 'gzip' end # Send the request to the server and read the response. response = @server.post2(soap_url, request.lstrip, headers) # decode if we have encoding content = decode(response) # 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) # repackage and rencode request with the new session id request = (Envelope % [@session_id, @batch_size, 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) end SoapResponse.new(content).parse end # decode gzip def decode(response) encoding = response['Content-Encoding'] # return body if no encoding if !encoding then return response.body end # decode gzip case encoding.strip when 'gzip' then begin gzr = Zlib::GzipReader.new(StringIO.new(response.body)) decoded = gzr.read ensure gzr.close end decoded else response.body end end # encode gzip def encode(request) return request if show_debug begin ostream = StringIO.new gzw = Zlib::GzipWriter.new(ostream) gzw.write(request) ostream.string ensure gzw.close end 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' end call_remote method, args[0] || [] end end end