| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517 | 
							- require 'active_resource'
 
- require 'active_support/core_ext/class/attribute_accessors'
 
- require 'digest/md5'
 
- require 'base64'
 
- module ShopifyAPI
 
-   METAFIELD_ENABLED_CLASSES = %w( Order Product CustomCollection SmartCollection Page Blog Article Variant)
 
-   EVENT_ENABLED_CLASSES = %w( Order Product CustomCollection SmartCollection Page Blog Article )
 
-   
 
-   module Countable
 
-     def count(options = {})
 
-       Integer(get(:count, options))
 
-     end
 
-   end
 
-   
 
-   module Metafields
 
-     def metafields
 
-       Metafield.find(:all, :params => {:resource => self.class.collection_name, :resource_id => id})
 
-     end
 
-     
 
-     def add_metafield(metafield)
 
-       raise ArgumentError, "You can only add metafields to resource that has been saved" if new?
 
-       
 
-       metafield.prefix_options = {
 
-         :resource => self.class.collection_name,
 
-         :resource_id => id
 
-       }
 
-       metafield.save
 
-       metafield
 
-     end
 
-   end
 
-   module Events
 
-     def events
 
-       Event.find(:all, :params => {:resource => self.class.collection_name, :resource_id => id})
 
-     end
 
-   end
 
-     
 
-   # 
 
-   #  The Shopify API authenticates each call via HTTP Authentication, using
 
-   #    * the application's API key as the username, and
 
-   #    * a hex digest of the application's shared secret and an 
 
-   #      authentication token as the password.
 
-   #  
 
-   #  Generation & acquisition of the beforementioned looks like this:
 
-   # 
 
-   #    0. Developer (that's you) registers Application (and provides a
 
-   #       callback url) and receives an API key and a shared secret
 
-   # 
 
-   #    1. User visits Application and are told they need to authenticate the
 
-   #       application first for read/write permission to their data (needs to
 
-   #       happen only once). User is asked for their shop url.
 
-   # 
 
-   #    2. Application redirects to Shopify : GET <user's shop url>/admin/api/auth?api_key=<API key>
 
-   #       (See Session#create_permission_url)
 
-   # 
 
-   #    3. User logs-in to Shopify, approves application permission request
 
-   # 
 
-   #    4. Shopify redirects to the Application's callback url (provided during
 
-   #       registration), including the shop's name, and an authentication token in the parameters:
 
-   #         GET client.com/customers?shop=snake-oil.myshopify.com&t=a94a110d86d2452eb3e2af4cfb8a3828
 
-   # 
 
-   #    5. Authentication password computed using the shared secret and the
 
-   #       authentication token (see Session#computed_password)
 
-   # 
 
-   #    6. Profit!
 
-   #       (API calls can now authenticate through HTTP using the API key, and
 
-   #       computed password)
 
-   # 
 
-   #  LoginController and ShopifyLoginProtection use the Session class to set Shopify::Base.site
 
-   #  so that all API calls are authorized transparently and end up just looking like this:
 
-   # 
 
-   #    # get 3 products
 
-   #    @products = ShopifyAPI::Product.find(:all, :params => {:limit => 3})
 
-   #    
 
-   #    # get latest 3 orders
 
-   #    @orders = ShopifyAPI::Order.find(:all, :params => {:limit => 3, :order => "created_at DESC" })
 
-   # 
 
-   #  As an example of what your LoginController should look like, take a look
 
-   #  at the following:
 
-   # 
 
-   #    class LoginController < ApplicationController
 
-   #      def index
 
-   #        # Ask user for their #{shop}.myshopify.com address
 
-   #      end
 
-   #    
 
-   #      def authenticate
 
-   #        redirect_to ShopifyAPI::Session.new(params[:shop]).create_permission_url
 
-   #      end
 
-   #    
 
-   #      # Shopify redirects the logged-in user back to this action along with
 
-   #      # the authorization token t.
 
-   #      # 
 
-   #      # This token is later combined with the developer's shared secret to form
 
-   #      # the password used to call API methods.
 
-   #      def finalize
 
-   #        shopify_session = ShopifyAPI::Session.new(params[:shop], params[:t])
 
-   #        if shopify_session.valid?
 
-   #          session[:shopify] = shopify_session
 
-   #          flash[:notice] = "Logged in to shopify store."
 
-   #    
 
-   #          return_address = session[:return_to] || '/home'
 
-   #          session[:return_to] = nil
 
-   #          redirect_to return_address
 
-   #        else
 
-   #          flash[:error] = "Could not log in to Shopify store."
 
-   #          redirect_to :action => 'index'
 
-   #        end
 
-   #      end
 
-   #    
 
-   #      def logout
 
-   #        session[:shopify] = nil
 
-   #        flash[:notice] = "Successfully logged out."
 
-   #    
 
-   #        redirect_to :action => 'index'
 
-   #      end
 
-   #    end
 
-   # 
 
-   class Session
 
-     cattr_accessor :api_key
 
-     cattr_accessor :secret
 
-     cattr_accessor :protocol 
 
-     self.protocol = 'https'
 
-     attr_accessor :url, :token, :name
 
-     
 
-     def self.setup(params)
 
-       params.each { |k,value| send("#{k}=", value) }
 
-     end
 
-     def initialize(url, token = nil, params = nil)
 
-       self.url, self.token = url, token
 
-       if params
 
-         unless self.class.validate_signature(params) && params[:timestamp].to_i > 24.hours.ago.utc.to_i
 
-           raise "Invalid Signature: Possible malicious login" 
 
-         end
 
-       end
 
-       self.class.prepare_url(self.url)
 
-     end
 
-     
 
-     def shop
 
-       Shop.current
 
-     end
 
-     
 
-     def create_permission_url
 
-       return nil if url.blank? || api_key.blank?
 
-       "http://#{url}/admin/api/auth?api_key=#{api_key}"
 
-     end
 
-     # Used by ActiveResource::Base to make all non-authentication API calls
 
-     # 
 
-     # (ShopifyAPI::Base.site set in ShopifyLoginProtection#shopify_session)
 
-     def site
 
-       "#{protocol}://#{api_key}:#{computed_password}@#{url}/admin"
 
-     end
 
-     def valid?
 
-       url.present? && token.present?
 
-     end
 
-     private
 
-     # The secret is computed by taking the shared_secret which we got when 
 
-     # registring this third party application and concating the request_to it, 
 
-     # and then calculating a MD5 hexdigest. 
 
-     def computed_password
 
-       Digest::MD5.hexdigest(secret + token.to_s)
 
-     end
 
-     
 
-     def self.prepare_url(url)
 
-       return nil if url.blank?
 
-       url.gsub!(/https?:\/\//, '')                            # remove http:// or https://
 
-       url.concat(".myshopify.com") unless url.include?('.')   # extend url to myshopify.com if no host is given
 
-     end
 
-     
 
-     def self.validate_signature(params)
 
-       return false unless signature = params[:signature]
 
-       sorted_params = params.except(:signature, :action, :controller).collect{|k,v|"#{k}=#{v}"}.sort.join
 
-       Digest::MD5.hexdigest(secret + sorted_params) == signature
 
-     end
 
-   end
 
-   
 
-   class Base < ActiveResource::Base
 
-     extend Countable
 
-   end
 
-   # Shop object. Use Shop.current to receive 
 
-   # the shop.
 
-   class Shop < Base
 
-     def self.current
 
-       find(:one, :from => "/admin/shop.#{format.extension}")
 
-     end
 
-     
 
-     def metafields
 
-       Metafield.find(:all)
 
-     end
 
-     
 
-     def add_metafield(metafield)
 
-       raise ArgumentError, "You can only add metafields to resource that has been saved" if new?      
 
-       metafield.save
 
-       metafield
 
-     end
 
-     
 
-     def events
 
-       Event.find(:all)
 
-     end
 
-   end               
 
-   # Custom collection
 
-   #
 
-   class CustomCollection < Base
 
-     def products
 
-       Product.find(:all, :params => {:collection_id => self.id})
 
-     end
 
-     
 
-     def add_product(product)
 
-       Collect.create(:collection_id => self.id, :product_id => product.id)
 
-     end
 
-     
 
-     def remove_product(product)
 
-       collect = Collect.find(:first, :params => {:collection_id => self.id, :product_id => product.id})
 
-       collect.destroy if collect
 
-     end
 
-   end                                                                 
 
-   
 
-   class SmartCollection < Base
 
-     def products
 
-       Product.find(:all, :params => {:collection_id => self.id})
 
-     end
 
-   end  
 
-   
 
-   # For adding/removing products from custom collections
 
-   class Collect < Base
 
-   end
 
-   class ShippingAddress < Base
 
-   end
 
-   class BillingAddress < Base
 
-   end         
 
-   class LineItem < Base 
 
-   end       
 
-   class ShippingLine < Base
 
-   end  
 
-   class NoteAttribute < Base
 
-   end
 
-   class Order < Base
 
-     def close; load_attributes_from_response(post(:close)); end
 
-     def open; load_attributes_from_response(post(:open)); end
 
-     def transactions
 
-       Transaction.find(:all, :params => { :order_id => id })
 
-     end
 
-     
 
-     def capture(amount = "")
 
-       Transaction.create(:amount => amount, :kind => "capture", :order_id => id)
 
-     end
 
-   end
 
-   
 
-   class Product < Base
 
-     # Share all items of this store with the 
 
-     # shopify marketplace
 
-     def self.share; post :share;  end    
 
-     def self.unshare; delete :share; end
 
-     # compute the price range
 
-     def price_range
 
-       prices = variants.collect(&:price)
 
-       format =  "%0.2f"
 
-       if prices.min != prices.max
 
-         "#{format % prices.min} - #{format % prices.max}"
 
-       else
 
-         format % prices.min
 
-       end
 
-     end
 
-     
 
-     def collections
 
-       CustomCollection.find(:all, :params => {:product_id => self.id})
 
-     end
 
-     
 
-     def smart_collections
 
-       SmartCollection.find(:all, :params => {:product_id => self.id})
 
-     end
 
-     
 
-     def add_to_collection(collection)
 
-       collection.add_product(self)
 
-     end
 
-     
 
-     def remove_from_collection(collection)
 
-       collection.remove_product(self)
 
-     end
 
-   end
 
-   
 
-   class Variant < Base
 
-     self.prefix = "/admin/products/:product_id/"
 
-     
 
-     def self.prefix(options={})
 
-       options[:product_id].nil? ? "/admin/" : "/admin/products/#{options[:product_id]}/"
 
-     end
 
-   end
 
-   
 
-   class Image < Base
 
-     self.prefix = "/admin/products/:product_id/"
 
-     
 
-     # generate a method for each possible image variant
 
-     [:pico, :icon, :thumb, :small, :compact, :medium, :large, :grande, :original].each do |m|
 
-       reg_exp_match = "/\\1_#{m}.\\2"
 
-       define_method(m) { src.gsub(/\/(.*)\.(\w{2,4})/, reg_exp_match) }
 
-     end
 
-     
 
-     def attach_image(data, filename = nil)
 
-       attributes['attachment'] = Base64.encode64(data)
 
-       attributes['filename'] = filename unless filename.nil?
 
-     end
 
-   end
 
-   class Transaction < Base
 
-     self.prefix = "/admin/orders/:order_id/"
 
-   end
 
-   
 
-   class Fulfillment < Base
 
-     self.prefix = "/admin/orders/:order_id/"
 
-   end
 
-   class Country < Base
 
-   end
 
-   class Page < Base
 
-   end
 
-   
 
-   class Blog < Base
 
-     def articles
 
-       Article.find(:all, :params => { :blog_id => id })
 
-     end
 
-   end
 
-   
 
-   class Article < Base
 
-     self.prefix = "/admin/blogs/:blog_id/"
 
-   end
 
-   
 
-   class Metafield < Base
 
-     self.prefix = "/admin/:resource/:resource_id/"
 
-     
 
-     # Hack to allow both Shop and other Metafields in through the same AR class
 
-     def self.prefix(options={})
 
-       options[:resource].nil? ? "/admin/" : "/admin/#{options[:resource]}/#{options[:resource_id]}/"
 
-     end
 
-             
 
-     def value
 
-       return if attributes["value"].nil?
 
-       attributes["value_type"] == "integer" ? attributes["value"].to_i : attributes["value"]
 
-     end
 
-   end
 
-   class Comment < Base 
 
-     def remove; load_attributes_from_response(post(:remove)); end
 
-     def ham; load_attributes_from_response(post(:ham)); end
 
-     def spam; load_attributes_from_response(post(:spam)); end
 
-     def approve; load_attributes_from_response(post(:approve)); end        
 
-   end
 
-   
 
-   class Province < Base
 
-     self.prefix = "/admin/countries/:country_id/"
 
-   end
 
-   
 
-   class Redirect < Base
 
-   end
 
-   
 
-   class Webhook < Base
 
-   end
 
-   
 
-   class Event < Base
 
-     self.prefix = "/admin/:resource/:resource_id/"
 
-     
 
-     # Hack to allow both Shop and other Events in through the same AR class
 
-     def self.prefix(options={})
 
-       options[:resource].nil? ? "/admin/" : "/admin/#{options[:resource]}/#{options[:resource_id]}/"
 
-     end
 
-   end
 
-   
 
-   # Assets represent the files that comprise your theme.
 
-   # There are different buckets which hold different kinds
 
-   # of assets, each corresponding to one of the folders
 
-   # within a theme's zip file: layout, templates, and
 
-   # assets. The full key of an asset always starts with the
 
-   # bucket name, and the path separator is a forward slash,
 
-   # like layout/theme.liquid or assets/bg-body.gif.
 
-   #
 
-   # Initialize with a key:
 
-   #   asset = ShopifyAPI::Asset.new(:key => 'assets/special.css')
 
-   # 
 
-   # Find by key:
 
-   #   asset = ShopifyAPI::Asset.find('assets/image.png')
 
-   # 
 
-   # Get the text or binary value:
 
-   #   asset.value # decodes from attachment attribute if necessary
 
-   # 
 
-   # You can provide new data for assets in a few different ways:
 
-   # 
 
-   #   * assign text data for the value directly:
 
-   #       asset.value = "div.special {color:red;}"
 
-   #     
 
-   #   * provide binary data for the value:
 
-   #       asset.attach(File.read('image.png'))
 
-   #     
 
-   #   * set a URL from which Shopify will fetch the value:
 
-   #       asset.src = "http://mysite.com/image.png"
 
-   #     
 
-   #   * set a source key of another of your assets from which
 
-   #     the value will be copied:
 
-   #       asset.source_key = "assets/another_image.png"
 
-   class Asset < Base
 
-     self.primary_key = 'key'
 
-     
 
-     # find an asset by key:
 
-     #   ShopifyAPI::Asset.find('layout/theme.liquid')
 
-     def self.find(*args)
 
-       if args[0].is_a?(Symbol)
 
-         super
 
-       else
 
-         params = {:asset => {:key => args[0]}}
 
-         params = params.merge(args[1][:params]) if args[1] && args[1][:params]
 
-         find(:one, :from => "/admin/assets.#{format.extension}", :params => params)
 
-       end
 
-     end
 
-     
 
-     # For text assets, Shopify returns the data in the 'value' attribute.
 
-     # For binary assets, the data is base-64-encoded and returned in the
 
-     # 'attachment' attribute. This accessor returns the data in both cases.
 
-     def value
 
-       attributes['value'] ||
 
-       (attributes['attachment'] ? Base64.decode64(attributes['attachment']) : nil)
 
-     end
 
-     
 
-     def attach(data)
 
-       self.attachment = Base64.encode64(data)
 
-     end
 
-     
 
-     def destroy #:nodoc:
 
-       connection.delete(element_path(:asset => {:key => key}), self.class.headers)
 
-     end
 
-     
 
-     def new? #:nodoc:
 
-       false
 
-     end
 
-     
 
-     def self.element_path(id, prefix_options = {}, query_options = nil) #:nodoc:
 
-       prefix_options, query_options = split_options(prefix_options) if query_options.nil?
 
-       "#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
 
-     end
 
-     
 
-     def method_missing(method_symbol, *arguments) #:nodoc:
 
-       if %w{value= attachment= src= source_key=}.include?(method_symbol)
 
-         wipe_value_attributes
 
-       end
 
-       super
 
-     end
 
-     
 
-     private
 
-     
 
-     def wipe_value_attributes
 
-       %w{value attachment src source_key}.each do |attr|
 
-         attributes.delete(attr)
 
-       end
 
-     end
 
-   end
 
-   
 
-   class RecurringApplicationCharge < Base
 
-     undef_method :test
 
-     def self.current
 
-       find(:all).find{|charge| charge.status == 'active'}
 
-     end
 
-     
 
-     def cancel
 
-       load_attributes_from_response(self.destroy)
 
-     end
 
-     
 
-     def activate
 
-       load_attributes_from_response(post(:activate))
 
-     end
 
-   end
 
-   class ApplicationCharge < Base
 
-     undef_method :test
 
-     def activate
 
-       load_attributes_from_response(post(:activate))
 
-     end
 
-   end
 
-   class ProductSearchEngine < Base
 
-   end
 
-   
 
-   class ScriptTag < Base
 
-   end
 
-   
 
-   # Include Metafields module in all enabled classes
 
-   METAFIELD_ENABLED_CLASSES.each do |klass|
 
-     "ShopifyAPI::#{klass}".constantize.send(:include, Metafields)
 
-   end
 
-   
 
-   # Include Events module in all enabled classes
 
-   EVENT_ENABLED_CLASSES.each do |klass|
 
-     "ShopifyAPI::#{klass}".constantize.send(:include, Events)
 
-   end
 
- end
 
 
  |