require 'net/https' require 'uri' require 'zlib' require 'stringio' require 'rexml/document' require 'builder' require 'oauth' # the 0.3.6 version must be checked out module RForce # Implements the connection to the SalesForce server. class Binding include RForce 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 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. If you pass an oauth hash, it # must contain the keys :consumer_key, :consumer_secret, # :access_token, :access_secret, and :login_url. def initialize(url, sid = nil, oauth = nil) @session_id = sid @oauth = oauth @batch_size = DEFAULT_BATCH_SIZE init_server(url) 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 def consumer OAuth::Consumer.new(@oauth[:consumer_key], @oauth[:consumer_secret], { :site => "https://login.salesforce.com", :signature_method => 'HMAC-SHA1', # this is default, but just for clarity :scheme => :body, :request_token_path => "/_nc_external/system/security/oauth/RequestTokenHandler", :access_token_path => "/_nc_external/system/security/oauth/AccessTokenHandler", :authorize_path => '/setup/secur/RemoteAccessAuthorizationPage.apexp', }) end # Log in to the server with OAuth, remembering # the session ID returned to us by SalesForce. def login_with_oauth # post is method of Oauth::AccessToken, @server is instance of it access_token = OAuth::AccessToken.new consumer, @oauth[:access_token], @oauth[:access_secret] result = access_token.post @oauth[:login_url] 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 => @session_id, :serverUrl => server_url} when Net::HTTPUnauthorized raise "Invalid OAuth tokens=#{@oauth.inspect}" else raise "Unexpected error: #{result.inspect}" end 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) @user = user @password = password response = call_remote(:login, [:username, user, :password, 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 # 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/0.4.1 LH' } 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 && @session_id && content =~ /sf:INVALID_SESSION_ID/ if @oauth login_with_oauth else login(@user, @password) end # 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.size == 1 && [Hash, Array].include?(args[0].class) raise 'Expected 1 Hash or Array argument' end call_remote method, args[0] end end end