lib/koala.rb in koala-1.0.0 vs lib/koala.rb in koala-1.1.0rc

- old
+ new

@@ -7,14 +7,18 @@ require 'openssl' require 'base64' # include koala modules require 'koala/http_services' +require 'koala/http_services/net_http_service' +require 'koala/oauth' require 'koala/graph_api' +require 'koala/graph_api_batch' require 'koala/rest_api' require 'koala/realtime_updates' require 'koala/test_users' +require 'koala/http_services' # add KoalaIO class require 'koala/uploadable_io' module Koala @@ -45,31 +49,28 @@ # Check for any 500 errors before parsing the body # since we're not guaranteed that the body is valid JSON # in the case of a server error raise APIError.new({"type" => "HTTP #{result.status.to_s}", "message" => "Response body: #{result.body}"}) if result.status >= 500 - # Parse the body as JSON and check for errors if provided a mechanism to do so + # parse the body as JSON and run it through the error checker (if provided) # Note: Facebook sometimes sends results like "true" and "false", which aren't strictly objects # and cause JSON.parse to fail -- so we account for that by wrapping the result in [] - body = response = JSON.parse("[#{result.body.to_s}]")[0] - if error_checking_block - yield(body) - end + body = JSON.parse("[#{result.body.to_s}]")[0] + yield body if error_checking_block - # now return the desired information - if options[:http_component] - result.send(options[:http_component]) - else - body - end + # if we want a component other than the body (e.g. redirect header for images), return that + options[:http_component] ? result.send(options[:http_component]) : body end end + # APIs + class GraphAPI < API include GraphAPIMethods + include GraphAPIBatchMethods end - + class RestAPI < API include RestAPIMethods end class GraphAndRestAPI < API @@ -85,210 +86,44 @@ include TestUserMethods # make the Graph API accessible in case someone wants to make other calls to interact with their users attr_reader :graph_api end + # Errors + class APIError < StandardError attr_accessor :fb_error_type def initialize(details = {}) self.fb_error_type = details["type"] super("#{fb_error_type}: #{details["message"]}") end end + end + class KoalaError < StandardError; end - class OAuth - attr_reader :app_id, :app_secret, :oauth_callback_url - def initialize(app_id, app_secret, oauth_callback_url = nil) - @app_id = app_id - @app_secret = app_secret - @oauth_callback_url = oauth_callback_url - end - - def get_user_info_from_cookie(cookie_hash) - # Parses the cookie set by the official Facebook JavaScript SDK. - # - # cookies should be a Hash, like the one Rails provides - # - # If the user is logged in via Facebook, we return a dictionary with the - # keys "uid" and "access_token". The former is the user's Facebook ID, - # and the latter can be used to make authenticated requests to the Graph API. - # If the user is not logged in, we return None. - # - # Download the official Facebook JavaScript SDK at - # http://github.com/facebook/connect-js/. Read more about Facebook - # authentication at http://developers.facebook.com/docs/authentication/. - - if fb_cookie = cookie_hash["fbs_" + @app_id.to_s] - # remove the opening/closing quote - fb_cookie = fb_cookie.gsub(/\"/, "") - - # since we no longer get individual cookies, we have to separate out the components ourselves - components = {} - fb_cookie.split("&").map {|param| param = param.split("="); components[param[0]] = param[1]} - - # generate the signature and make sure it matches what we expect - auth_string = components.keys.sort.collect {|a| a == "sig" ? nil : "#{a}=#{components[a]}"}.reject {|a| a.nil?}.join("") - sig = Digest::MD5.hexdigest(auth_string + @app_secret) - sig == components["sig"] && (components["expires"] == "0" || Time.now.to_i < components["expires"].to_i) ? components : nil - end - end - alias_method :get_user_info_from_cookies, :get_user_info_from_cookie - - def get_user_from_cookie(cookies) - if info = get_user_info_from_cookies(cookies) - string = info["uid"] - end - end - alias_method :get_user_from_cookies, :get_user_from_cookie - - # URLs - - def url_for_oauth_code(options = {}) - # for permissions, see http://developers.facebook.com/docs/authentication/permissions - permissions = options[:permissions] - scope = permissions ? "&scope=#{permissions.is_a?(Array) ? permissions.join(",") : permissions}" : "" - display = options.has_key?(:display) ? "&display=#{options[:display]}" : "" - - callback = options[:callback] || @oauth_callback_url - raise ArgumentError, "url_for_oauth_code must get a callback either from the OAuth object or in the options!" unless callback - - # Creates the URL for oauth authorization for a given callback and optional set of permissions - "https://#{GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}#{scope}#{display}" - end - - def url_for_access_token(code, options = {}) - # Creates the URL for the token corresponding to a given code generated by Facebook - callback = options[:callback] || @oauth_callback_url - raise ArgumentError, "url_for_access_token must get a callback either from the OAuth object or in the parameters!" unless callback - "https://#{GRAPH_SERVER}/oauth/access_token?client_id=#{@app_id}&redirect_uri=#{callback}&client_secret=#{@app_secret}&code=#{code}" - end - - def get_access_token_info(code, options = {}) - # convenience method to get a parsed token from Facebook for a given code - # should this require an OAuth callback URL? - get_token_from_server({:code => code, :redirect_uri => @oauth_callback_url}, false, options) - end - - def get_access_token(code, options = {}) - # upstream methods will throw errors if needed - if info = get_access_token_info(code, options) - string = info["access_token"] - end - end - - def get_app_access_token_info(options = {}) - # convenience method to get a the application's sessionless access token - get_token_from_server({:type => 'client_cred'}, true, options) - end - - def get_app_access_token(options = {}) - if info = get_app_access_token_info(options) - string = info["access_token"] - end - end - - # Originally provided directly by Facebook, however this has changed - # as their concept of crypto changed. For historic purposes, this is their proposal: - # https://developers.facebook.com/docs/authentication/canvas/encryption_proposal/ - # Currently see https://github.com/facebook/php-sdk/blob/master/src/facebook.php#L758 - # for a more accurate reference implementation strategy. - def parse_signed_request(input) - encoded_sig, encoded_envelope = input.split('.', 2) - signature = base64_url_decode(encoded_sig).unpack("H*").first - envelope = JSON.parse(base64_url_decode(encoded_envelope)) - - raise "SignedRequest: Unsupported algorithm #{envelope['algorithm']}" if envelope['algorithm'] != 'HMAC-SHA256' - - # now see if the signature is valid (digest, key, data) - hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, @app_secret, encoded_envelope.tr("-_", "+/")) - raise 'SignedRequest: Invalid signature' if (signature != hmac) - - return envelope - end - - # from session keys - def get_token_info_from_session_keys(sessions, options = {}) - # fetch the OAuth tokens from Facebook - response = fetch_token_string({ - :type => 'client_cred', - :sessions => sessions.join(",") - }, true, "exchange_sessions", options) - - # Facebook returns an empty body in certain error conditions - if response == "" - raise APIError.new({ - "type" => "ArgumentError", - "message" => "get_token_from_session_key received an error (empty response body) for sessions #{sessions.inspect}!" - }) - end - - JSON.parse(response) - end - - def get_tokens_from_session_keys(sessions, options = {}) - # get the original hash results - results = get_token_info_from_session_keys(sessions, options) - # now recollect them as just the access tokens - results.collect { |r| r ? r["access_token"] : nil } - end - - def get_token_from_session_key(session, options = {}) - # convenience method for a single key - # gets the overlaoded strings automatically - get_tokens_from_session_keys([session], options)[0] - end - - protected - - def get_token_from_server(args, post = false, options = {}) - # fetch the result from Facebook's servers - result = fetch_token_string(args, post, "access_token", options) - - # if we have an error, parse the error JSON and raise an error - raise APIError.new((JSON.parse(result)["error"] rescue nil) || {}) if result =~ /error/ - - # otherwise, parse the access token - parse_access_token(result) - end - - def parse_access_token(response_text) - components = response_text.split("&").inject({}) do |hash, bit| - key, value = bit.split("=") - hash.merge!(key => value) - end - components - end - - def fetch_token_string(args, post = false, endpoint = "access_token", options = {}) - Koala.make_request("/oauth/#{endpoint}", { - :client_id => @app_id, - :client_secret => @app_secret - }.merge!(args), post ? "post" : "get", {:use_ssl => true}.merge!(options)).body - end - - # base 64 - # directly from https://github.com/facebook/crypto-request-examples/raw/master/sample.rb - def base64_url_decode(str) - str += '=' * (4 - str.length.modulo(4)) - Base64.decode64(str.tr('-_', '+/')) - end - end + # Make an api request using the provided api service or one passed by the caller + def self.make_request(path, args, verb, options = {}) + http_service = options.delete(:http_service) || Koala.http_service + options = options.merge(:use_ssl => true) if @always_use_ssl + http_service.make_request(path, args, verb, options) end - class KoalaError< StandardError; end - # finally, set up the http service Koala methods used to make requests # you can use your own (for HTTParty, etc.) by calling Koala.http_service = YourModule - def self.http_service=(service) - self.send(:include, service) + class << self + attr_accessor :http_service + attr_accessor :always_use_ssl + attr_accessor :base_http_service end + Koala.base_http_service = NetHTTPService # by default, try requiring Typhoeus -- if that works, use it # if you have Typheous and don't want to use it (or want another service), # you can run Koala.http_service = NetHTTPService (or MyHTTPService) begin + require 'koala/http_services/typhoeus_service' Koala.http_service = TyphoeusService rescue LoadError - Koala.http_service = NetHTTPService + Koala.http_service = Koala.base_http_service end end