module RForce # Implements the connection to the SalesForce server. class Binding include RForce DEFAULT_BATCH_SIZE = 10 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 xmlns:spartner="urn:sobject.partner.soap.sforce.com"> %s %d %s %s HERE AssignmentRuleHeaderUsingRuleId = '%s' AssignmentRuleHeaderUsingDefaultRule = 'true' MruHeader = 'true' ClientIdHeader = '%s' # Connect to the server securely. def initialize(url, sid = nil) init_server(url) @session_id = sid @batch_size = DEFAULT_BATCH_SIZE end def show_debug ENV['SHOWSOAP'] == 'true' end def init_server(url) @url = URI.parse(url) @server = Net::HTTP.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 # Log in to the server and remember the session ID # returned to us by SalesForce. def login(user, password) @user = user @password = password response = call_remote(:login, [:username, user, :password, password]) raise "Incorrect user name / password [#{response[:Fault][:faultstring]}]" unless response.loginResponse result = response[:loginResponse][:result] @session_id = result[:sessionId] init_server(result[:serverUrl]) response 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) # Create XML text from the arguments. expanded = '' @builder = Builder::XmlMarkup.new(:target => expanded) expand(@builder, {method => args}, 'urn:partner.soap.sforce.com') 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(@url.path, 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(@url.path, 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': 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.size == 1 && [Hash, Array].include?(args[0].class) raise 'Expected 1 Hash or Array argument' end call_remote method, args[0] end end end