class RForce::Binding

Implements the connection to the Salesforce server.

Constants

AssignmentRuleHeaderUsingDefaultRule
AssignmentRuleHeaderUsingRuleId
ClientIdHeader
DEFAULT_BATCH_SIZE

Increase the maximum fetch size to 2000, as allowed by Salesforce Added by Raymond Gao

Envelope

Fill in the guts of this typical SOAP envelope with the session ID and the body of the SOAP request.

MruHeader
QueryOptions

Attributes

assignment_rule_id[RW]
batch_size[RW]
client_id[RW]
trigger_auto_response_email[RW]
trigger_other_email[RW]
trigger_user_email[RW]
update_mru[RW]
url[RW]
use_default_rule[RW]

Public Class Methods

new(url, sid = nil, oauth = nil, proxy = nil, logger = nil) click to toggle source

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 user:pass@example.com:port

if a logger is specified, it will be used for very verbose SOAP logging

# File lib/rforce/binding.rb, line 56
def initialize(url, sid = nil, oauth = nil, proxy = nil, logger = nil)
  @session_id = sid
  @oauth = oauth
  @proxy = proxy
  @batch_size = DEFAULT_BATCH_SIZE
  @logger = logger
  @url = URI.parse(url)
end

Public Instance Methods

call_remote(method, args) { |: ["soap.com", fallback_soap_url]| ... } click to toggle source

Call a method on the remote server. Arguments can be a hash or (if order is important) an array of alternating keys and values.

# File lib/rforce/binding.rb, line 138
def call_remote(method, args)
  @server ||= create_server(@url)

  # 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", fallback_soap_url]

  # Create XML text from the arguments.
  expanded = ''
  @builder = Builder::XmlMarkup.new(:target => expanded)
  expand(@builder, {method => args}, urn)

  extra_headers = ""

  # QueryOptions is not valid when making an Apex Webservice SOAP call
  if !block_given?
    extra_headers << (QueryOptions % @batch_size)
  end

  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 << '<partner:EmailHeader soap:mustUnderstand="1">'

    extra_headers << '<partner:triggerUserEmail>true</partner:triggerUserEmail>' if trigger_user_email
    extra_headers << '<partner:triggerOtherEmail>true</partner:triggerOtherEmail>' if trigger_other_email
    extra_headers << '<partner:triggerAutoResponseEmail>true</partner:triggerAutoResponseEmail>' if trigger_auto_response_email

    extra_headers << '</partner:EmailHeader>'
  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
  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.
  @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/
    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
create_server(url) click to toggle source
# File lib/rforce/binding.rb, line 69
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

  # run ruby with -d or env variable SHOWSOAP=true to see SOAP wiredumps.
  server.set_debug_output $stderr if show_debug

  return server
end
decode(response) click to toggle source

decode gzip

# File lib/rforce/binding.rb, line 235
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(request) click to toggle source

encode gzip

# File lib/rforce/binding.rb, line 257
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
fix_encoding(string) click to toggle source

fix invalid US-ASCII strings by applying the correct encoding on ruby 1.9+

# File lib/rforce/binding.rb, line 271
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
login(user, password) click to toggle source

Log in to the server with a user name and password, remembering the session ID returned to us by Salesforce.

# File lib/rforce/binding.rb, line 82
def login(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]
  @session_id = result[:sessionId]
  @url        = URI.parse(result[:serverUrl])
  @server     = create_server(@url)

  return response
end
login_with_oauth() click to toggle source

Log in to the server with OAuth, remembering the session ID returned to us by Salesforce.

# File lib/rforce/binding.rb, line 100
def login_with_oauth
  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
    @session_id = doc.elements['*/sessionId'].text
    @url        = URI.parse(doc.elements['*/serverUrl'].text)
    @server     = access

    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
end
method_missing(method, *args) click to toggle source

Turns method calls on this object into remote SOAP calls.

# File lib/rforce/binding.rb, line 294
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
show_debug() click to toggle source
# File lib/rforce/binding.rb, line 65
def show_debug
  ENV['SHOWSOAP'] == 'true'
end