koala 1.0.0 → 1.1.0rc
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +12 -0
- data/.gitignore +2 -1
- data/CHANGELOG +18 -0
- data/autotest/discover.rb +1 -0
- data/koala.gemspec +5 -5
- data/lib/koala.rb +29 -194
- data/lib/koala/graph_api.rb +71 -31
- data/lib/koala/graph_api_batch.rb +151 -0
- data/lib/koala/http_services.rb +10 -112
- data/lib/koala/http_services/net_http_service.rb +87 -0
- data/lib/koala/http_services/typhoeus_service.rb +37 -0
- data/lib/koala/oauth.rb +181 -0
- data/lib/koala/realtime_updates.rb +5 -14
- data/lib/koala/rest_api.rb +13 -8
- data/lib/koala/uploadable_io.rb +35 -7
- data/readme.md +19 -7
- data/spec/cases/api_base_spec.rb +2 -2
- data/spec/cases/graph_api_batch_spec.rb +600 -0
- data/spec/cases/http_services/http_service_spec.rb +76 -1
- data/spec/cases/http_services/net_http_service_spec.rb +164 -48
- data/spec/cases/http_services/typhoeus_service_spec.rb +27 -19
- data/spec/cases/koala_spec.rb +55 -0
- data/spec/cases/test_users_spec.rb +1 -1
- data/spec/cases/uploadable_io_spec.rb +56 -14
- data/spec/fixtures/mock_facebook_responses.yml +89 -5
- data/spec/support/graph_api_shared_examples.rb +34 -7
- data/spec/support/mock_http_service.rb +54 -56
- data/spec/support/rest_api_shared_examples.rb +131 -7
- data/spec/support/setup_mocks_or_live.rb +3 -3
- metadata +36 -24
@@ -0,0 +1,151 @@
|
|
1
|
+
module Koala
|
2
|
+
module Facebook
|
3
|
+
class BatchOperation
|
4
|
+
attr_reader :access_token, :http_options, :post_processing, :files
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
@args = (options[:args] || {}).dup # because we modify it below
|
8
|
+
@access_token = options[:access_token]
|
9
|
+
@http_options = (options[:http_options] || {}).dup # dup because we modify it below
|
10
|
+
@batch_args = @http_options.delete(:batch_args) || {}
|
11
|
+
@url = options[:url]
|
12
|
+
@method = options[:method].to_sym
|
13
|
+
@post_processing = options[:post_processing]
|
14
|
+
|
15
|
+
process_binary_args
|
16
|
+
|
17
|
+
raise Koala::KoalaError, "Batch operations require an access token, none provided." unless @access_token
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_batch_params(main_access_token)
|
21
|
+
# set up the arguments
|
22
|
+
args_string = Koala.http_service.encode_params(@access_token == main_access_token ? @args : @args.merge(:access_token => @access_token))
|
23
|
+
|
24
|
+
response = {
|
25
|
+
:method => @method,
|
26
|
+
:relative_url => @url,
|
27
|
+
}
|
28
|
+
|
29
|
+
# handle batch-level arguments, such as name, depends_on, and attached_files
|
30
|
+
@batch_args[:attached_files] = @files.keys.join(",") if @files
|
31
|
+
response.merge!(@batch_args) if @batch_args
|
32
|
+
|
33
|
+
# for get and delete, we append args to the URL string
|
34
|
+
# otherwise, they go in the body
|
35
|
+
if args_string.length > 0
|
36
|
+
if args_in_url?
|
37
|
+
response[:relative_url] += (@url =~ /\?/ ? "&" : "?") + args_string if args_string.length > 0
|
38
|
+
else
|
39
|
+
response[:body] = args_string if args_string.length > 0
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
response
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
|
48
|
+
def process_binary_args
|
49
|
+
# collect binary files
|
50
|
+
@args.each_pair do |key, value|
|
51
|
+
if UploadableIO.binary_content?(value)
|
52
|
+
@files ||= {}
|
53
|
+
# we use object_id to ensure unique file identifiers across multiple batch operations
|
54
|
+
# remove it from the original hash and add it to the file store
|
55
|
+
id = "file#{GraphAPI.batch_calls.length}_#{@files.keys.length}"
|
56
|
+
@files[id] = @args.delete(key).is_a?(UploadableIO) ? value : UploadableIO.new(value)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def args_in_url?
|
62
|
+
@method == :get || @method == :delete
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
module GraphAPIBatchMethods
|
67
|
+
def self.included(base)
|
68
|
+
base.class_eval do
|
69
|
+
# batch mode flags
|
70
|
+
def self.batch_mode?
|
71
|
+
!!@batch_mode
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.batch_calls
|
75
|
+
raise KoalaError, "GraphAPI.batch_calls accessed when not in batch block!" unless batch_mode?
|
76
|
+
@batch_calls
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.batch(http_options = {}, &block)
|
80
|
+
@batch_mode = true
|
81
|
+
@batch_http_options = http_options
|
82
|
+
@batch_calls = []
|
83
|
+
yield
|
84
|
+
begin
|
85
|
+
results = batch_api(@batch_calls)
|
86
|
+
ensure
|
87
|
+
@batch_mode = false
|
88
|
+
end
|
89
|
+
results
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.batch_api(batch_calls)
|
93
|
+
return [] unless batch_calls.length > 0
|
94
|
+
# Facebook requires a top-level access token
|
95
|
+
|
96
|
+
# Get the access token for the user and start building a hash to store params
|
97
|
+
# Turn the call args collected into what facebook expects
|
98
|
+
args = {}
|
99
|
+
access_token = args["access_token"] = batch_calls.first.access_token
|
100
|
+
args['batch'] = batch_calls.map { |batch_op|
|
101
|
+
args.merge!(batch_op.files) if batch_op.files
|
102
|
+
batch_op.to_batch_params(access_token)
|
103
|
+
}.to_json
|
104
|
+
|
105
|
+
# Make the POST request for the batch call
|
106
|
+
# batch operations have to go over SSL, but since there's an access token, that secures that
|
107
|
+
result = Koala.make_request('/', args, 'post', @batch_http_options)
|
108
|
+
# Raise an error if we get a 500
|
109
|
+
raise APIError.new("type" => "HTTP #{result.status.to_s}", "message" => "Response body: #{result.body}") if result.status >= 500
|
110
|
+
|
111
|
+
response = JSON.parse(result.body.to_s)
|
112
|
+
# raise an error if we get a Batch API error message
|
113
|
+
raise APIError.new("type" => "Error #{response["error"]}", "message" => response["error_description"]) if response.is_a?(Hash) && response["error"]
|
114
|
+
|
115
|
+
# otherwise, map the results with post-processing included
|
116
|
+
index = 0 # keep compat with ruby 1.8 - no with_index for map
|
117
|
+
response.map do |call_result|
|
118
|
+
# Get the options hash
|
119
|
+
batch_op = batch_calls[index]
|
120
|
+
index += 1
|
121
|
+
|
122
|
+
if call_result
|
123
|
+
# (see note in regular api method about JSON parsing)
|
124
|
+
body = JSON.parse("[#{call_result['body'].to_s}]")[0]
|
125
|
+
unless call_result["code"].to_i >= 500 || error = GraphAPI.check_response(body)
|
126
|
+
# Get the HTTP component they want
|
127
|
+
data = case batch_op.http_options[:http_component]
|
128
|
+
when :status
|
129
|
+
call_result["code"].to_i
|
130
|
+
when :headers
|
131
|
+
# facebook returns the headers as an array of k/v pairs, but we want a regular hash
|
132
|
+
call_result['headers'].inject({}) { |headers, h| headers[h['name']] = h['value']; headers}
|
133
|
+
else
|
134
|
+
body
|
135
|
+
end
|
136
|
+
|
137
|
+
# process it if we are given a block to process with
|
138
|
+
batch_op.post_processing ? batch_op.post_processing.call(data) : data
|
139
|
+
else
|
140
|
+
error || APIError.new({"type" => "HTTP #{call_result["code"].to_s}", "message" => "Response body: #{body}"})
|
141
|
+
end
|
142
|
+
else
|
143
|
+
nil
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
data/lib/koala/http_services.rb
CHANGED
@@ -13,134 +13,32 @@ module Koala
|
|
13
13
|
def self.included(base)
|
14
14
|
base.class_eval do
|
15
15
|
class << self
|
16
|
-
attr_accessor :always_use_ssl
|
16
|
+
attr_accessor :always_use_ssl, :proxy, :timeout, :ca_path, :ca_file
|
17
17
|
end
|
18
|
-
|
18
|
+
|
19
19
|
def self.server(options = {})
|
20
20
|
"#{options[:beta] ? "beta." : ""}#{options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER}"
|
21
21
|
end
|
22
|
-
|
23
|
-
protected
|
24
22
|
|
25
|
-
def self.params_require_multipart?(param_hash)
|
26
|
-
param_hash.any? { |key, value| value.kind_of?(Koala::UploadableIO) }
|
27
|
-
end
|
28
|
-
|
29
|
-
def self.multipart_requires_content_type?
|
30
|
-
true
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
module NetHTTPService
|
37
|
-
# this service uses Net::HTTP to send requests to the graph
|
38
|
-
def self.included(base)
|
39
|
-
base.class_eval do
|
40
|
-
require "net/http" unless defined?(Net::HTTP)
|
41
|
-
require "net/https"
|
42
|
-
require "net/http/post/multipart"
|
43
|
-
|
44
|
-
include Koala::HTTPService
|
45
|
-
|
46
|
-
def self.make_request(path, args, verb, options = {})
|
47
|
-
# We translate args to a valid query string. If post is specified,
|
48
|
-
# we send a POST request to the given path with the given arguments.
|
49
|
-
|
50
|
-
# by default, we use SSL only for private requests
|
51
|
-
# this makes public requests faster
|
52
|
-
private_request = args["access_token"] || @always_use_ssl || options[:use_ssl]
|
53
|
-
|
54
|
-
# if the verb isn't get or post, send it as a post argument
|
55
|
-
args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
|
56
|
-
|
57
|
-
http = create_http(server(options), private_request, options)
|
58
|
-
http.use_ssl = true if private_request
|
59
|
-
|
60
|
-
result = http.start do |http|
|
61
|
-
response, body = if verb == "post"
|
62
|
-
if params_require_multipart? args
|
63
|
-
http.request Net::HTTP::Post::Multipart.new path, encode_multipart_params(args)
|
64
|
-
else
|
65
|
-
http.post(path, encode_params(args))
|
66
|
-
end
|
67
|
-
else
|
68
|
-
http.get("#{path}?#{encode_params(args)}")
|
69
|
-
end
|
70
|
-
|
71
|
-
Koala::Response.new(response.code.to_i, body, response)
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
protected
|
76
23
|
def self.encode_params(param_hash)
|
77
24
|
# unfortunately, we can't use to_query because that's Rails, not Ruby
|
78
25
|
# if no hash (e.g. no auth token) return empty string
|
79
26
|
((param_hash || {}).collect do |key_and_value|
|
80
|
-
key_and_value[1] = key_and_value[1].to_json
|
27
|
+
key_and_value[1] = key_and_value[1].to_json unless key_and_value[1].is_a? String
|
81
28
|
"#{key_and_value[0].to_s}=#{CGI.escape key_and_value[1]}"
|
82
29
|
end).join("&")
|
83
30
|
end
|
31
|
+
|
32
|
+
protected
|
84
33
|
|
85
|
-
def self.
|
86
|
-
|
87
|
-
[key, value.kind_of?(Koala::UploadableIO) ? value.to_upload_io : value]
|
88
|
-
end.flatten]
|
89
|
-
end
|
90
|
-
|
91
|
-
def self.create_http(server, private_request, options)
|
92
|
-
if options[:proxy]
|
93
|
-
proxy = URI.parse(options[:proxy])
|
94
|
-
http = Net::HTTP.new(server, private_request ? 443 : nil,
|
95
|
-
proxy.host, proxy.port, proxy.user, proxy.password)
|
96
|
-
else
|
97
|
-
http = Net::HTTP.new(server, private_request ? 443 : nil)
|
98
|
-
end
|
99
|
-
if options[:timeout]
|
100
|
-
http.open_timeout = options[:timeout]
|
101
|
-
http.read_timeout = options[:timeout]
|
102
|
-
end
|
103
|
-
http
|
104
|
-
end
|
105
|
-
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
module TyphoeusService
|
111
|
-
# this service uses Typhoeus to send requests to the graph
|
112
|
-
|
113
|
-
def self.included(base)
|
114
|
-
base.class_eval do
|
115
|
-
require "typhoeus" unless defined?(Typhoeus)
|
116
|
-
include Typhoeus
|
117
|
-
|
118
|
-
include Koala::HTTPService
|
119
|
-
|
120
|
-
def self.make_request(path, args, verb, options = {})
|
121
|
-
# if the verb isn't get or post, send it as a post argument
|
122
|
-
args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
|
123
|
-
|
124
|
-
# switch any UploadableIOs to the files Typhoeus expects
|
125
|
-
args.each_pair {|key, value| args[key] = value.to_file if value.is_a?(UploadableIO)}
|
126
|
-
|
127
|
-
# you can pass arguments directly to Typhoeus using the :typhoeus_options key
|
128
|
-
typhoeus_options = {:params => args}.merge(options[:typhoeus_options] || {})
|
129
|
-
|
130
|
-
# by default, we use SSL only for private requests (e.g. with access token)
|
131
|
-
# this makes public requests faster
|
132
|
-
prefix = (args["access_token"] || @always_use_ssl || options[:use_ssl]) ? "https" : "http"
|
133
|
-
|
134
|
-
response = self.send(verb, "#{prefix}://#{server(options)}#{path}", typhoeus_options)
|
135
|
-
Koala::Response.new(response.code, response.body, response.headers_hash)
|
34
|
+
def self.params_require_multipart?(param_hash)
|
35
|
+
param_hash.any? { |key, value| value.kind_of?(Koala::UploadableIO) }
|
136
36
|
end
|
137
|
-
|
138
|
-
private
|
139
|
-
|
37
|
+
|
140
38
|
def self.multipart_requires_content_type?
|
141
|
-
|
39
|
+
true
|
142
40
|
end
|
143
|
-
end
|
41
|
+
end
|
144
42
|
end
|
145
43
|
end
|
146
44
|
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require "net/http" unless defined?(Net::HTTP)
|
2
|
+
require "net/https"
|
3
|
+
require "net/http/post/multipart"
|
4
|
+
|
5
|
+
module Koala
|
6
|
+
module NetHTTPService
|
7
|
+
# this service uses Net::HTTP to send requests to the graph
|
8
|
+
include Koala::HTTPService
|
9
|
+
|
10
|
+
def self.make_request(path, args, verb, options = {})
|
11
|
+
# We translate args to a valid query string. If post is specified,
|
12
|
+
# we send a POST request to the given path with the given arguments.
|
13
|
+
|
14
|
+
# by default, we use SSL only for private requests
|
15
|
+
# this makes public requests faster
|
16
|
+
private_request = args["access_token"] || @always_use_ssl || options[:use_ssl]
|
17
|
+
|
18
|
+
# if proxy/timeout options aren't passed, check if defaults are set
|
19
|
+
options[:proxy] ||= proxy
|
20
|
+
options[:timeout] ||= timeout
|
21
|
+
|
22
|
+
# if the verb isn't get or post, send it as a post argument
|
23
|
+
args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
|
24
|
+
|
25
|
+
http = create_http(server(options), private_request, options)
|
26
|
+
|
27
|
+
result = http.start do |http|
|
28
|
+
response, body = if verb == "post"
|
29
|
+
if params_require_multipart? args
|
30
|
+
http.request Net::HTTP::Post::Multipart.new path, encode_multipart_params(args)
|
31
|
+
else
|
32
|
+
http.post(path, encode_params(args))
|
33
|
+
end
|
34
|
+
else
|
35
|
+
http.get("#{path}?#{encode_params(args)}")
|
36
|
+
end
|
37
|
+
|
38
|
+
Koala::Response.new(response.code.to_i, body, response)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
def self.encode_params(param_hash)
|
44
|
+
# unfortunately, we can't use to_query because that's Rails, not Ruby
|
45
|
+
# if no hash (e.g. no auth token) return empty string
|
46
|
+
((param_hash || {}).collect do |key_and_value|
|
47
|
+
key_and_value[1] = key_and_value[1].to_json if key_and_value[1].class != String
|
48
|
+
"#{key_and_value[0].to_s}=#{CGI.escape key_and_value[1]}"
|
49
|
+
end).join("&")
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.encode_multipart_params(param_hash)
|
53
|
+
Hash[*param_hash.collect do |key, value|
|
54
|
+
[key, value.kind_of?(Koala::UploadableIO) ? value.to_upload_io : value]
|
55
|
+
end.flatten]
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.create_http(server, private_request, options)
|
59
|
+
if options[:proxy]
|
60
|
+
proxy = URI.parse(options[:proxy])
|
61
|
+
http = Net::HTTP.new(server, private_request ? 443 : nil,
|
62
|
+
proxy.host, proxy.port, proxy.user, proxy.password)
|
63
|
+
else
|
64
|
+
http = Net::HTTP.new(server, private_request ? 443 : nil)
|
65
|
+
end
|
66
|
+
|
67
|
+
if options[:timeout]
|
68
|
+
http.open_timeout = options[:timeout]
|
69
|
+
http.read_timeout = options[:timeout]
|
70
|
+
end
|
71
|
+
|
72
|
+
# For HTTPS requests, set the proper CA certificates
|
73
|
+
if private_request
|
74
|
+
http.use_ssl = true
|
75
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
76
|
+
|
77
|
+
options[:ca_file] ||= ca_file
|
78
|
+
http.ca_file = options[:ca_file] if options[:ca_file] && File.exists?(options[:ca_file])
|
79
|
+
|
80
|
+
options[:ca_path] ||= ca_path
|
81
|
+
http.ca_path = options[:ca_path] if options[:ca_path] && Dir.exists?(options[:ca_path])
|
82
|
+
end
|
83
|
+
|
84
|
+
http
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "typhoeus" unless defined?(Typhoeus)
|
2
|
+
|
3
|
+
module Koala
|
4
|
+
module TyphoeusService
|
5
|
+
# this service uses Typhoeus to send requests to the graph
|
6
|
+
include Typhoeus
|
7
|
+
include Koala::HTTPService
|
8
|
+
|
9
|
+
def self.make_request(path, args, verb, options = {})
|
10
|
+
# if the verb isn't get or post, send it as a post argument
|
11
|
+
args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
|
12
|
+
|
13
|
+
# switch any UploadableIOs to the files Typhoeus expects
|
14
|
+
args.each_pair {|key, value| args[key] = value.to_file if value.is_a?(UploadableIO)}
|
15
|
+
|
16
|
+
# you can pass arguments directly to Typhoeus using the :typhoeus_options key
|
17
|
+
typhoeus_options = {:params => args}.merge(options[:typhoeus_options] || {})
|
18
|
+
|
19
|
+
# if proxy/timeout options aren't passed, check if defaults are set
|
20
|
+
typhoeus_options[:proxy] ||= proxy
|
21
|
+
typhoeus_options[:timeout] ||= timeout
|
22
|
+
|
23
|
+
# by default, we use SSL only for private requests (e.g. with access token)
|
24
|
+
# this makes public requests faster
|
25
|
+
prefix = (args["access_token"] || @always_use_ssl || options[:use_ssl]) ? "https" : "http"
|
26
|
+
|
27
|
+
response = Typhoeus::Request.send(verb, "#{prefix}://#{server(options)}#{path}", typhoeus_options)
|
28
|
+
Koala::Response.new(response.code, response.body, response.headers_hash)
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
def self.multipart_requires_content_type?
|
34
|
+
false # Typhoeus handles multipart file types, we don't have to require it
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/koala/oauth.rb
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
module Koala
|
2
|
+
module Facebook
|
3
|
+
class OAuth
|
4
|
+
attr_reader :app_id, :app_secret, :oauth_callback_url
|
5
|
+
def initialize(app_id, app_secret, oauth_callback_url = nil)
|
6
|
+
@app_id = app_id
|
7
|
+
@app_secret = app_secret
|
8
|
+
@oauth_callback_url = oauth_callback_url
|
9
|
+
end
|
10
|
+
|
11
|
+
def get_user_info_from_cookie(cookie_hash)
|
12
|
+
# Parses the cookie set by the official Facebook JavaScript SDK.
|
13
|
+
#
|
14
|
+
# cookies should be a Hash, like the one Rails provides
|
15
|
+
#
|
16
|
+
# If the user is logged in via Facebook, we return a dictionary with the
|
17
|
+
# keys "uid" and "access_token". The former is the user's Facebook ID,
|
18
|
+
# and the latter can be used to make authenticated requests to the Graph API.
|
19
|
+
# If the user is not logged in, we return None.
|
20
|
+
#
|
21
|
+
# Download the official Facebook JavaScript SDK at
|
22
|
+
# http://github.com/facebook/connect-js/. Read more about Facebook
|
23
|
+
# authentication at http://developers.facebook.com/docs/authentication/.
|
24
|
+
|
25
|
+
if fb_cookie = cookie_hash["fbs_" + @app_id.to_s]
|
26
|
+
# remove the opening/closing quote
|
27
|
+
fb_cookie = fb_cookie.gsub(/\"/, "")
|
28
|
+
|
29
|
+
# since we no longer get individual cookies, we have to separate out the components ourselves
|
30
|
+
components = {}
|
31
|
+
fb_cookie.split("&").map {|param| param = param.split("="); components[param[0]] = param[1]}
|
32
|
+
|
33
|
+
# generate the signature and make sure it matches what we expect
|
34
|
+
auth_string = components.keys.sort.collect {|a| a == "sig" ? nil : "#{a}=#{components[a]}"}.reject {|a| a.nil?}.join("")
|
35
|
+
sig = Digest::MD5.hexdigest(auth_string + @app_secret)
|
36
|
+
sig == components["sig"] && (components["expires"] == "0" || Time.now.to_i < components["expires"].to_i) ? components : nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
alias_method :get_user_info_from_cookies, :get_user_info_from_cookie
|
40
|
+
|
41
|
+
def get_user_from_cookie(cookies)
|
42
|
+
if info = get_user_info_from_cookies(cookies)
|
43
|
+
string = info["uid"]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
alias_method :get_user_from_cookies, :get_user_from_cookie
|
47
|
+
|
48
|
+
# URLs
|
49
|
+
|
50
|
+
def url_for_oauth_code(options = {})
|
51
|
+
# for permissions, see http://developers.facebook.com/docs/authentication/permissions
|
52
|
+
permissions = options[:permissions]
|
53
|
+
scope = permissions ? "&scope=#{permissions.is_a?(Array) ? permissions.join(",") : permissions}" : ""
|
54
|
+
display = options.has_key?(:display) ? "&display=#{options[:display]}" : ""
|
55
|
+
|
56
|
+
callback = options[:callback] || @oauth_callback_url
|
57
|
+
raise ArgumentError, "url_for_oauth_code must get a callback either from the OAuth object or in the options!" unless callback
|
58
|
+
|
59
|
+
# Creates the URL for oauth authorization for a given callback and optional set of permissions
|
60
|
+
"https://#{GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}#{scope}#{display}"
|
61
|
+
end
|
62
|
+
|
63
|
+
def url_for_access_token(code, options = {})
|
64
|
+
# Creates the URL for the token corresponding to a given code generated by Facebook
|
65
|
+
callback = options[:callback] || @oauth_callback_url
|
66
|
+
raise ArgumentError, "url_for_access_token must get a callback either from the OAuth object or in the parameters!" unless callback
|
67
|
+
"https://#{GRAPH_SERVER}/oauth/access_token?client_id=#{@app_id}&redirect_uri=#{callback}&client_secret=#{@app_secret}&code=#{code}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_access_token_info(code, options = {})
|
71
|
+
# convenience method to get a parsed token from Facebook for a given code
|
72
|
+
# should this require an OAuth callback URL?
|
73
|
+
get_token_from_server({:code => code, :redirect_uri => @oauth_callback_url}, false, options)
|
74
|
+
end
|
75
|
+
|
76
|
+
def get_access_token(code, options = {})
|
77
|
+
# upstream methods will throw errors if needed
|
78
|
+
if info = get_access_token_info(code, options)
|
79
|
+
string = info["access_token"]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def get_app_access_token_info(options = {})
|
84
|
+
# convenience method to get a the application's sessionless access token
|
85
|
+
get_token_from_server({:type => 'client_cred'}, true, options)
|
86
|
+
end
|
87
|
+
|
88
|
+
def get_app_access_token(options = {})
|
89
|
+
if info = get_app_access_token_info(options)
|
90
|
+
string = info["access_token"]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Originally provided directly by Facebook, however this has changed
|
95
|
+
# as their concept of crypto changed. For historic purposes, this is their proposal:
|
96
|
+
# https://developers.facebook.com/docs/authentication/canvas/encryption_proposal/
|
97
|
+
# Currently see https://github.com/facebook/php-sdk/blob/master/src/facebook.php#L758
|
98
|
+
# for a more accurate reference implementation strategy.
|
99
|
+
def parse_signed_request(input)
|
100
|
+
encoded_sig, encoded_envelope = input.split('.', 2)
|
101
|
+
signature = base64_url_decode(encoded_sig).unpack("H*").first
|
102
|
+
envelope = JSON.parse(base64_url_decode(encoded_envelope))
|
103
|
+
|
104
|
+
raise "SignedRequest: Unsupported algorithm #{envelope['algorithm']}" if envelope['algorithm'] != 'HMAC-SHA256'
|
105
|
+
|
106
|
+
# now see if the signature is valid (digest, key, data)
|
107
|
+
hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, @app_secret, encoded_envelope.tr("-_", "+/"))
|
108
|
+
raise 'SignedRequest: Invalid signature' if (signature != hmac)
|
109
|
+
|
110
|
+
return envelope
|
111
|
+
end
|
112
|
+
|
113
|
+
# from session keys
|
114
|
+
def get_token_info_from_session_keys(sessions, options = {})
|
115
|
+
# fetch the OAuth tokens from Facebook
|
116
|
+
response = fetch_token_string({
|
117
|
+
:type => 'client_cred',
|
118
|
+
:sessions => sessions.join(",")
|
119
|
+
}, true, "exchange_sessions", options)
|
120
|
+
|
121
|
+
# Facebook returns an empty body in certain error conditions
|
122
|
+
if response == ""
|
123
|
+
raise APIError.new({
|
124
|
+
"type" => "ArgumentError",
|
125
|
+
"message" => "get_token_from_session_key received an error (empty response body) for sessions #{sessions.inspect}!"
|
126
|
+
})
|
127
|
+
end
|
128
|
+
|
129
|
+
JSON.parse(response)
|
130
|
+
end
|
131
|
+
|
132
|
+
def get_tokens_from_session_keys(sessions, options = {})
|
133
|
+
# get the original hash results
|
134
|
+
results = get_token_info_from_session_keys(sessions, options)
|
135
|
+
# now recollect them as just the access tokens
|
136
|
+
results.collect { |r| r ? r["access_token"] : nil }
|
137
|
+
end
|
138
|
+
|
139
|
+
def get_token_from_session_key(session, options = {})
|
140
|
+
# convenience method for a single key
|
141
|
+
# gets the overlaoded strings automatically
|
142
|
+
get_tokens_from_session_keys([session], options)[0]
|
143
|
+
end
|
144
|
+
|
145
|
+
protected
|
146
|
+
|
147
|
+
def get_token_from_server(args, post = false, options = {})
|
148
|
+
# fetch the result from Facebook's servers
|
149
|
+
result = fetch_token_string(args, post, "access_token", options)
|
150
|
+
|
151
|
+
# if we have an error, parse the error JSON and raise an error
|
152
|
+
raise APIError.new((JSON.parse(result)["error"] rescue nil) || {}) if result =~ /error/
|
153
|
+
|
154
|
+
# otherwise, parse the access token
|
155
|
+
parse_access_token(result)
|
156
|
+
end
|
157
|
+
|
158
|
+
def parse_access_token(response_text)
|
159
|
+
components = response_text.split("&").inject({}) do |hash, bit|
|
160
|
+
key, value = bit.split("=")
|
161
|
+
hash.merge!(key => value)
|
162
|
+
end
|
163
|
+
components
|
164
|
+
end
|
165
|
+
|
166
|
+
def fetch_token_string(args, post = false, endpoint = "access_token", options = {})
|
167
|
+
Koala.make_request("/oauth/#{endpoint}", {
|
168
|
+
:client_id => @app_id,
|
169
|
+
:client_secret => @app_secret
|
170
|
+
}.merge!(args), post ? "post" : "get", {:use_ssl => true}.merge!(options)).body
|
171
|
+
end
|
172
|
+
|
173
|
+
# base 64
|
174
|
+
# directly from https://github.com/facebook/crypto-request-examples/raw/master/sample.rb
|
175
|
+
def base64_url_decode(str)
|
176
|
+
str += '=' * (4 - str.length.modulo(4))
|
177
|
+
Base64.decode64(str.tr('-_', '+/'))
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|