# The MIT License (MIT) # # Copyright (C) 2014 hellosign.com # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. require 'faraday' require 'faraday/multipart' require 'multi_json' require 'mime/types' require 'hello_sign/error' require 'hello_sign/configuration' require 'hello_sign/resource' require 'hello_sign/api' require 'logger' module HelloSign # You'll need the HelloSign::Client to do just about everything, from creating # signatures to updating account information. # # @author [hellosign] class Client include Api::Account include Api::SignatureRequest include Api::Template include Api::Team include Api::UnclaimedDraft include Api::Embedded include Api::OAuth include Api::ApiApp include Api::BulkSendJob attr_accessor :end_point, :oauth_end_point, :api_version, :user_agent, :client_id, :client_secret, :email_address, :password, :api_key, :auth_token, :logging, :log_level, :proxy_uri, :proxy_user, :proxy_pass, :timeout ERRORS = { 400 => Error::BadRequest, 401 => Error::Unauthorized, 402 => Error::PaidApiPlanRequired, 403 => Error::Forbidden, 404 => Error::NotFound, 405 => Error::MethodNotAllowed, 409 => Error::Conflict, 410 => Error::Gone, 429 => Error::ExceededRate, 500 => Error::InternalServerError, 502 => Error::BadGateway, 503 => Error::ServiceUnavailable } # Initiates a new HelloSign Client # @option opts [String] email_address The account's email address. (optional) # @option opts [String] password The account's password, if authenticating with an email_address. (optional) # @option opts [String] api_key The account's API key. # # @return [HelloSign::Client] a new HelloSign::Client # # @example Authenticating with username and password # client = HelloSign::Client.new email_address: 'me@example.com', password: 'mypassword' # # @example Authenticating with API key # client = HelloSign::Client.new api_key: '1234567890123456789012345678901234567890123456789012345678901234' def initialize(opts={}) options = HelloSign.options.merge(opts) HelloSign::Configuration::VALID_OPTIONS_KEYS.each do |key| self.send("#{key}=", options[key]) end end # Makes an HTTP GET request # @param path [String] Relative path of the request. # @option options [Hash] params Params of the URL. def get(path, options={}) response = request(path, :get, options) validate response parsed_response = parse response data = { headers: response.headers, body: parsed_response } end # Makes an HTTP POST request # @param path [String] Relative path of the request. # @option options [Hash] params Params of the URL. # @option options [Hash] body Body of the request. def post(path, options={}) response = request(path, :post, options) validate response parsed_response = parse response data = { headers: response.headers, body: parsed_response } end # Makes an HTTP PUT request # @param path [String] Relative path of the request. # @option options [Hash] params Params of the URL. # @option options [Hash] body Body of the request. def put(path, options={}) response = request(path, :put, options) validate response responsed_response = parse response data = { headers: response.headers, body: parsed_response } end # Makes an HTTP DELETE request # @param path [String] Relative path of the request. # @option options [Hash] Params of the URL. def delete(path, options={}) response = request(path, :delete, options) validate response parsed_response = parse response data = { headers: response.headers, body: parsed_response } end private def request(path, method, options={}) make_connection(options).send method do |request| if options[:oauth_request] request.url path, options[:params] else request.url "#{api_version}#{path}", options[:params] end request.body = options[:body] end end def make_connection options url = options[:oauth_request] ? oauth_end_point : end_point if @logging logger = Logger.new(STDOUT) logger.level = @log_level end connection = Faraday.new(:url => url, :headers => { user_agent: user_agent }) do |faraday| faraday.request :multipart faraday.request :url_encoded faraday.response :logger, logger if @logging faraday.adapter :net_http faraday.options[:timeout] = timeout if timeout end if options[:no_auth] elsif auth_token connection.request :authorization, 'Bearer', -> { auth_token } elsif api_key connection.request :authorization, :basic, api_key, '' elsif email_address connection.request :authorization, :basic, email_address, password else end if proxy_uri connection.options.proxy = { :uri => proxy_uri.nil? ? nil : URI(proxy_uri), :user => proxy_user, :password => proxy_pass } end connection end def validate(response) if response.status >= 400 error_class = ERRORS[response.status] || HelloSign::Error::UnknownError error = error_class.new(response.status, response.body, response.to_hash[:url].to_s) raise error end end def parse(response) if response['content-type'] == 'application/pdf' response.body elsif response['content-type'] == 'application/zip' response.body elsif response.body.strip.empty? {} else MultiJson.load response.body.strip end end def MIMEfromName(name) parts = name.split('.') # defaults to pdf if no extension provided if parts.length < 2 return 'application/pdf' end extension = parts[-1] types = MIME::Types.type_for(extension) types[0] end def MIMEfromIO(file) begin path = File.path file MIMEfromName path # in case of error in type detection, return default type rescue return 'application/pdf' end end def prepare_files(opts) if opts[:files] opts[:files].each_with_index do |file, index| if file.is_a? String if File.file?(file) mime_type = MIMEfromName file opts[:"file[#{index}]"] = Faraday::UploadIO.new(file, mime_type) else raise HelloSign::Error::FileNotFound.new "#{file} was not found on the filesystem" end elsif file.is_a? File mime_type = MIMEfromIO file opts[:"file[#{index}]"] = Faraday::UploadIO.new(file, mime_type) elsif file.is_a? StringIO opts[:"file[#{index}]"] = Faraday::UploadIO.new(file, 'application/pdf') elsif defined? ActionDispatch::Http::UploadedFile if file.is_a? ActionDispatch::Http::UploadedFile mime_type = MIMEfromIO file opts[:"file[#{index}]"] = UploadIO.new(file.tempfile, mime_type, file.original_filename) end else raise HelloSign::Error::NotSupportedType.new "#{file.class} is not a supported. Must be a string or ActionDispatch::Http::UploadedFile" end end opts.delete(:files) elsif opts[:file_urls] opts[:file_urls].each_with_index do |file, index| opts[:"file_url[#{index}]"] = file end opts.delete(:file_urls) end end # Takes options given to an API endpoint as a hash, plus an array of parameters (as symbols), and creates a # query string out of any listed parameters present (that also have values) def create_query_string(opts, parameterList) result = "" is_first = true parameterList.each do |param| if (opts.has_key?(param) && opts[param]) if (!is_first) result += "&" end is_first = false result += "#{param}=#{opts[param]}" end end if (result.length > 0) result = "?" + result end result end def create_search_string(raw_string) raw_string.tr(" ", "+") end def prepare_signers(opts) if opts[:signers] prepare opts, :signers elsif opts[:signer_group] prepare_signer_group opts, :signer_group end end def prepare_ccs(opts) prepare opts, :ccs end def prepare_templates(opts) prepare opts, :template_ids end def prepare_signer_roles(opts) prepare opts, :signer_roles end def prepare_attachments(opts) prepare opts, :attachments end def prepare_form_fields(opts) if (opts[:form_fields_per_document] and opts[:form_fields_per_document].is_a? Array) opts[:form_fields_per_document] = MultiJson.dump(opts[:form_fields_per_document]) end # ignore if it's already a string, or not present end def prepare_custom_fields(opts) if (opts[:custom_fields] and opts[:custom_fields].is_a? Array) opts[:custom_fields] = MultiJson.dump(opts[:custom_fields]) end # ignore if it's already a string, or not present end def prepare_merge_fields(opts) if (opts[:merge_fields] and opts[:merge_fields].is_a? Array) opts[:merge_fields] = MultiJson.dump(opts[:merge_fields]) end # ignore if it's already a string, or not present end def prepare_bulk_signers(opts) if opts[:signer_file] file = opts[:signer_file] mime_type = MIMEfromIO file opts[:signer_file] = Faraday::UploadIO.new(file, mime_type) elsif opts[:signer_list] opts[:signer_list] = MultiJson.dump(opts[:signer_list]) else raise HelloSign::Error::NotSupportedType.new "Upload a CSV file or JSON list of signers" end end def prepare(opts, key) return unless opts[key] opts[key].each_with_index do |value, index| if value.is_a? String opts[:"#{key}[#{index}]"] = value else if value[:role] opts[:"#{key}[#{value[:role]}]"] = value value.delete(:role) else opts[:"#{key}[#{index}]"] = value end end end opts.delete(key) end def prepare_signer_group(opts, key) opts[key].each_with_index do |value, index| if value[:role] group_index_or_role = value[:role] else group_index_or_role = index end opts[:"signers[#{group_index_or_role}][group]"] = value[:group_name] opts[key] = value[:signers] prepare_signers_for_group(value[:signers], group_index_or_role, opts) end opts.delete(key) end def prepare_signers_for_group(signers, group_index_or_role, opts) signers.each_with_index do |signer, index| signer.each do |param, data| opts[:"signers[#{group_index_or_role}][#{index}][#{param}]"] = data end end end end end