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