123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193 |
- require 'openssl'
- require 'rack'
- module ShopifyAPI
- class ValidationException < StandardError
- end
- class Session
- cattr_accessor :api_key, :secret, :myshopify_domain
- self.myshopify_domain = 'myshopify.com'
- attr_accessor :domain, :token, :name, :extra
- attr_reader :api_version
- alias_method :url, :domain
- class << self
- def setup(params)
- params.each { |k,value| public_send("#{k}=", value) }
- end
- def temp(domain:, token:, api_version: ShopifyAPI::Base.api_version, &block)
- session = new(domain: domain, token: token, api_version: api_version)
- with_session(session, &block)
- end
- def with_session(session, &_block)
- original_session = extract_current_session
- original_user = ShopifyAPI::Base.user
- original_password = ShopifyAPI::Base.password
- begin
- ShopifyAPI::Base.clear_session
- ShopifyAPI::Base.activate_session(session)
- yield
- ensure
- ShopifyAPI::Base.activate_session(original_session)
- ShopifyAPI::Base.user = original_user
- ShopifyAPI::Base.password = original_password
- end
- end
- def with_version(api_version, &block)
- original_session = extract_current_session
- session = new(domain: original_session.site, token: original_session.token, api_version: api_version)
- with_session(session, &block)
- end
- def prepare_domain(domain)
- return nil if domain.blank?
- # remove http:// or https://
- domain = domain.strip.gsub(%r{\Ahttps?://}, '')
- # extract host, removing any username, password or path
- shop = URI.parse("https://#{domain}").host
- # extract subdomain of .myshopify.com
- if (idx = shop.index("."))
- shop = shop.slice(0, idx)
- end
- return nil if shop.empty?
- "#{shop}.#{myshopify_domain}"
- rescue URI::InvalidURIError
- nil
- end
- def validate_signature(params)
- params = (params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params).with_indifferent_access
- return false unless (signature = params[:hmac])
- calculated_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA256'), secret, encoded_params_for_signature(params))
- Rack::Utils.secure_compare(calculated_signature, signature)
- end
- private
- def encoded_params_for_signature(params)
- params = params.except(:signature, :hmac, :action, :controller)
- params.map{|k,v| "#{URI.escape(k.to_s, '&=%')}=#{URI.escape(v.to_s, '&%')}"}.sort.join('&')
- end
- def extract_current_session
- site = ShopifyAPI::Base.site.to_s
- token = ShopifyAPI::Base.headers['X-Shopify-Access-Token']
- version = ShopifyAPI::Base.api_version
- new(domain: site, token: token, api_version: version)
- end
- end
- def initialize(domain:, token:, api_version: ShopifyAPI::Base.api_version, extra: {})
- self.domain = self.class.prepare_domain(domain)
- self.api_version = api_version
- self.token = token
- self.extra = extra
- end
- def create_permission_url(scope, redirect_uri, options = {})
- params = { client_id: api_key, scope: scope.join(','), redirect_uri: redirect_uri }
- params[:state] = options[:state] if options[:state]
- construct_oauth_url("authorize", params)
- end
- def request_token(params)
- return token if token
- unless self.class.validate_signature(params) && params[:timestamp].to_i > 24.hours.ago.utc.to_i
- raise ShopifyAPI::ValidationException, "Invalid Signature: Possible malicious login"
- end
- response = access_token_request(params[:code])
- if response.code == "200"
- self.extra = JSON.parse(response.body)
- self.token = extra.delete('access_token')
- if (expires_in = extra.delete('expires_in'))
- extra['expires_at'] = Time.now.utc.to_i + expires_in
- end
- token
- else
- raise RuntimeError, response.msg
- end
- end
- def shop
- Shop.current
- end
- def site
- "https://#{domain}"
- end
- def api_version=(version)
- @api_version = ApiVersion::NullVersion.matches?(version) ? ApiVersion::NullVersion : ApiVersion.find_version(version)
- end
- def valid?
- domain.present? && token.present? && api_version.is_a?(ApiVersion)
- end
- def expires_in
- return unless expires_at.present?
- [0, expires_at.to_i - Time.now.utc.to_i].max
- end
- def expires_at
- return unless extra.present?
- @expires_at ||= Time.at(extra['expires_at']).utc
- end
- def expired?
- return false if expires_in.nil?
- expires_in <= 0
- end
- def hash
- state.hash
- end
- def ==(other)
- self.class == other.class && state == other.state
- end
- alias_method :eql?, :==
- protected
- def state
- [domain, token, api_version, extra]
- end
- private
- def parameterize(params)
- URI.escape(params.collect { |k, v| "#{k}=#{v}" }.join('&'))
- end
- def access_token_request(code)
- uri = URI.parse(construct_oauth_url('access_token'))
- https = Net::HTTP.new(uri.host, uri.port)
- https.use_ssl = true
- request = Net::HTTP::Post.new(uri.request_uri)
- request.set_form_data('client_id' => api_key, 'client_secret' => secret, 'code' => code)
- https.request(request)
- end
- def construct_oauth_url(path, query_params = {})
- query_string = "?#{parameterize(query_params)}" unless query_params.empty?
- "https://#{domain}/admin/oauth/#{path}#{query_string}"
- end
- end
- end
|