koala 1.0.0 → 1.1.0rc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|