lib/databasedotcom/client.rb in databasedotcom-1.0.9 vs lib/databasedotcom/client.rb in databasedotcom-1.1.0
- old
+ new
@@ -9,10 +9,12 @@
attr_accessor :client_id
# The client secret (aka "Consumer Secret" to use for OAuth2 authentication)
attr_accessor :client_secret
# The OAuth access token in use by the client
attr_accessor :oauth_token
+ # The OAuth refresh token in use by the client
+ attr_accessor :refresh_token
# The base URL to the authenticated user's SalesForce instance
attr_accessor :instance_url
# If true, print API debugging information to stdout. Defaults to false.
attr_accessor :debugging
# The host to use for OAuth2 authentication. Defaults to +login.salesforce.com+
@@ -77,11 +79,11 @@
# Authenticate to the Force.com API. _options_ is a Hash, interpreted as follows:
#
# * If _options_ contains the keys <tt>:username</tt> and <tt>:password</tt>, those credentials are used to authenticate. In this case, the value of <tt>:password</tt> may need to include a concatenated security token, if required by your Salesforce org
# * If _options_ contains the key <tt>:provider</tt>, it is assumed to be the hash returned by Omniauth from a successful web-based OAuth2 authentication
- # * If _options_ contains the keys <tt>:token</tt> and <tt>:instance_url</tt>, those are assumed to be a valid OAuth2 token and instance URL for a Salesforce account, obtained from an external source
+ # * If _options_ contains the keys <tt>:token</tt> and <tt>:instance_url</tt>, those are assumed to be a valid OAuth2 token and instance URL for a Salesforce account, obtained from an external source. _options_ may also optionally contain the key <tt>:refresh_token</tt>
#
# Raises SalesForceError if an error occurs
def authenticate(options = nil)
if user_and_pass?(options)
req = Net::HTTP.new(self.host, 443)
@@ -91,23 +93,24 @@
path = "/services/oauth2/token?grant_type=password&client_id=#{self.client_id}&client_secret=#{client_secret}&username=#{user}&password=#{pass}"
log_request("https://#{self.host}/#{path}")
result = req.post(path, "")
log_response(result)
raise SalesForceError.new(result) unless result.is_a?(Net::HTTPOK)
- json = JSON.parse(result.body)
- @user_id = json["id"].match(/\/([^\/]+)$/)[1] rescue nil
- self.instance_url = json["instance_url"]
- self.oauth_token = json["access_token"]
+ self.username = user
+ self.password = pass
+ parse_auth_response(result.body)
elsif options.is_a?(Hash)
if options.has_key?("provider")
@user_id = options["extra"]["user_hash"]["user_id"] rescue nil
self.instance_url = options["credentials"]["instance_url"]
self.oauth_token = options["credentials"]["token"]
+ self.refresh_token = options["credentials"]["refresh_token"]
else
raise ArgumentError unless options.has_key?(:token) && options.has_key?(:instance_url)
self.instance_url = options[:instance_url]
self.oauth_token = options[:token]
+ self.refresh_token = options[:refresh_token]
end
end
self.version = "22.0" unless self.version
@@ -260,85 +263,114 @@
# Performs an HTTP GET request to the specified path (relative to self.instance_url). Query parameters are included from _parameters_. The required
# +Authorization+ header is automatically included, as are any additional headers specified in _headers_. Returns the HTTPResult if it is of type
# HTTPSuccess- raises SalesForceError otherwise.
def http_get(path, parameters={}, headers={})
- req = Net::HTTP.new(URI.parse(self.instance_url).host, 443)
- req.use_ssl = true
- path_parameters = (parameters || {}).collect { |k, v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}" }.join('&')
- encoded_path = [URI.escape(path), path_parameters.empty? ? nil : path_parameters].compact.join('?')
- log_request(encoded_path)
- result = req.get(encoded_path, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
- log_response(result)
- raise SalesForceError.new(result) unless result.is_a?(Net::HTTPSuccess)
- result
+ with_encoded_path_and_checked_response(path, parameters) do |encoded_path|
+ https_request.get(encoded_path, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
+ end
end
# Performs an HTTP DELETE request to the specified path (relative to self.instance_url). Query parameters are included from _parameters_. The required
# +Authorization+ header is automatically included, as are any additional headers specified in _headers_. Returns the HTTPResult if it is of type
# HTTPSuccess- raises SalesForceError otherwise.
def http_delete(path, parameters={}, headers={})
- req = Net::HTTP.new(URI.parse(self.instance_url).host, 443)
- req.use_ssl = true
- path_parameters = (parameters || {}).collect { |k, v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}" }.join('&')
- encoded_path = [URI.escape(path), path_parameters.empty? ? nil : path_parameters].compact.join('?')
- log_request(encoded_path)
- result = req.delete(encoded_path, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
- log_response(result)
- raise SalesForceError.new(result) unless result.is_a?(Net::HTTPNoContent)
- result
+ with_encoded_path_and_checked_response(path, parameters, {:expected_result_class => Net::HTTPNoContent}) do |encoded_path|
+ https_request.delete(encoded_path, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
+ end
end
# Performs an HTTP POST request to the specified path (relative to self.instance_url). The body of the request is taken from _data_.
# Query parameters are included from _parameters_. The required +Authorization+ header is automatically included, as are any additional
# headers specified in _headers_. Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.
def http_post(path, data=nil, parameters={}, headers={})
- req = Net::HTTP.new(URI.parse(self.instance_url).host, 443)
- req.use_ssl = true
- path_parameters = (parameters || {}).collect { |k, v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}" }.join('&')
- encoded_path = [URI.escape(path), path_parameters.empty? ? nil : path_parameters].compact.join('?')
- log_request(encoded_path, data)
- result = req.post(encoded_path, data, {"Content-Type" => data ? "application/json" : "text/plain", "Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
- log_response(result)
- raise SalesForceError.new(result) unless result.is_a?(Net::HTTPSuccess)
- result
+ with_encoded_path_and_checked_response(path, parameters, {:data => data}) do |encoded_path|
+ https_request.post(encoded_path, data, {"Content-Type" => data ? "application/json" : "text/plain", "Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
+ end
end
# Performs an HTTP PATCH request to the specified path (relative to self.instance_url). The body of the request is taken from _data_.
# Query parameters are included from _parameters_. The required +Authorization+ header is automatically included, as are any additional
# headers specified in _headers_. Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.
def http_patch(path, data=nil, parameters={}, headers={})
- req = Net::HTTP.new(URI.parse(self.instance_url).host, 443)
- req.use_ssl = true
- path_parameters = (parameters || {}).collect { |k, v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}" }.join('&')
- encoded_path = [URI.escape(path), path_parameters.empty? ? nil : path_parameters].compact.join('?')
- log_request(encoded_path, data)
- result = req.send_request("PATCH", encoded_path, data, {"Content-Type" => data ? "application/json" : "text/plain", "Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
- log_response(result)
- raise SalesForceError.new(result) unless result.is_a?(Net::HTTPSuccess)
- result
+ with_encoded_path_and_checked_response(path, parameters, {:data => data}) do |encoded_path|
+ https_request.send_request("PATCH", encoded_path, data, {"Content-Type" => data ? "application/json" : "text/plain", "Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
+ end
end
# Performs an HTTP POST request to the specified path (relative to self.instance_url), using Content-Type multiplart/form-data.
# The parts of the body of the request are taken from parts_. Query parameters are included from _parameters_. The required
# +Authorization+ header is automatically included, as are any additional headers specified in _headers_.
# Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.
def http_multipart_post(path, parts, parameters={}, headers={})
- req = Net::HTTP.new(URI.parse(self.instance_url).host, 443)
- req.use_ssl = true
- path_parameters = (parameters || {}).collect { |k, v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}" }.join('&')
- encoded_path = [URI.escape(path), path_parameters.empty? ? nil : path_parameters].compact.join('?')
- log_request(encoded_path)
- result = req.request(Net::HTTP::Post::Multipart.new(encoded_path, parts, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers)))
- log_response(result)
- raise SalesForceError.new(result) unless result.is_a?(Net::HTTPSuccess)
- result
+ with_encoded_path_and_checked_response(path, parameters) do |encoded_path|
+ https_request.request(Net::HTTP::Post::Multipart.new(encoded_path, parts, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers)))
+ end
end
private
+ def with_encoded_path_and_checked_response(path, parameters, opts = {})
+ ensure_expected_response(opts[:expected_result_class]) do
+ with_logging(encode_path_with_params(path, parameters), opts[:data]) do |encoded_path|
+ yield(encoded_path)
+ end
+ end
+ end
+
+ def with_logging(encoded_path, optional_data = nil)
+ log_request(encoded_path, optional_data)
+ response = yield encoded_path
+ log_response(response)
+ response
+ end
+
+ def ensure_expected_response(expected_result_class)
+ yield.tap do |response|
+ unless response.is_a?(expected_result_class || Net::HTTPSuccess)
+ if response.is_a?(Net::HTTPUnauthorized)
+ if self.refresh_token
+ with_encoded_path_and_checked_response("/services/oauth2/token", { :grant_type => "refresh_token", :refresh_token => self.refresh_token, :client_id => self.client_id, :client_secret => self.client_secret}) do |encoded_path|
+ response = https_request(self.host).post(encoded_path, nil)
+ if response.is_a?(Net::HTTPOK)
+ parse_auth_response(response.body)
+ end
+ response
+ end
+ elsif self.username && self.password
+ with_encoded_path_and_checked_response("/services/oauth2/token", { :grant_type => "password", :username => self.username, :password => self.password, :client_id => self.client_id, :client_secret => self.client_secret}) do |encoded_path|
+ response = https_request(self.host).post(encoded_path, nil)
+ if response.is_a?(Net::HTTPOK)
+ parse_auth_response(response.body)
+ end
+ response
+ end
+ end
+
+ if response.is_a?(Net::HTTPSuccess)
+ response = yield
+ end
+ end
+ end
+
+ raise SalesForceError.new(response) unless response.is_a?(expected_result_class || Net::HTTPSuccess)
+ end
+ end
+
+ def https_request(host=nil)
+ Net::HTTP.new(host || URI.parse(self.instance_url).host, 443).tap{|n| n.use_ssl = true }
+ end
+
+ def encode_path_with_params(path, parameters={})
+ [URI.escape(path), encode_parameters(parameters)].reject{|el| el.empty?}.join('?')
+ end
+
+ def encode_parameters(parameters={})
+ (parameters || {}).collect { |k, v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}" }.join('&')
+ end
+
def log_request(path, data=nil)
puts "***** REQUEST: #{path.include?(':') ? path : URI.join(self.instance_url, path)}#{data ? " => #{data}" : ''}" if self.debugging
end
def log_response(result)
@@ -427,8 +459,15 @@
label.gsub(' ', '_')
end
def user_and_pass?(options)
(self.username && self.password) || (options && options[:username] && options[:password])
+ end
+
+ def parse_auth_response(body)
+ json = JSON.parse(body)
+ @user_id = json["id"].match(/\/([^\/]+)$/)[1] rescue nil
+ self.instance_url = json["instance_url"]
+ self.oauth_token = json["access_token"]
end
end
end