koala 1.3.0rc1 → 1.3.0rc2

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.
Files changed (45) hide show
  1. data/.gitignore +3 -1
  2. data/.travis.yml +4 -0
  3. data/.yardopts +3 -0
  4. data/CHANGELOG +7 -0
  5. data/Gemfile +14 -0
  6. data/Guardfile +6 -0
  7. data/lib/koala.rb +16 -97
  8. data/lib/koala/api.rb +93 -0
  9. data/lib/koala/api/batch_operation.rb +83 -0
  10. data/lib/koala/api/graph_api.rb +476 -0
  11. data/lib/koala/{graph_batch_api.rb → api/graph_batch_api.rb} +20 -16
  12. data/lib/koala/api/graph_collection.rb +107 -0
  13. data/lib/koala/api/legacy.rb +26 -0
  14. data/lib/koala/{rest_api.rb → api/rest_api.rb} +33 -3
  15. data/lib/koala/http_service.rb +69 -19
  16. data/lib/koala/http_service/multipart_request.rb +41 -0
  17. data/lib/koala/http_service/response.rb +18 -0
  18. data/lib/koala/http_service/uploadable_io.rb +187 -0
  19. data/lib/koala/oauth.rb +117 -14
  20. data/lib/koala/realtime_updates.rb +89 -51
  21. data/lib/koala/test_users.rb +109 -33
  22. data/lib/koala/utils.rb +4 -0
  23. data/lib/koala/version.rb +1 -1
  24. data/spec/cases/api_spec.rb +19 -12
  25. data/spec/cases/graph_api_batch_spec.rb +41 -41
  26. data/spec/cases/http_service_spec.rb +1 -22
  27. data/spec/cases/legacy_spec.rb +107 -0
  28. data/spec/cases/multipart_request_spec.rb +5 -5
  29. data/spec/cases/oauth_spec.rb +9 -9
  30. data/spec/cases/realtime_updates_spec.rb +154 -47
  31. data/spec/cases/test_users_spec.rb +268 -219
  32. data/spec/fixtures/mock_facebook_responses.yml +10 -6
  33. data/spec/support/graph_api_shared_examples.rb +17 -12
  34. data/spec/support/koala_test.rb +1 -1
  35. data/spec/support/mock_http_service.rb +2 -2
  36. data/spec/support/rest_api_shared_examples.rb +1 -1
  37. metadata +82 -104
  38. data/lib/koala/batch_operation.rb +0 -74
  39. data/lib/koala/graph_api.rb +0 -289
  40. data/lib/koala/graph_collection.rb +0 -63
  41. data/lib/koala/multipart_request.rb +0 -35
  42. data/lib/koala/uploadable_io.rb +0 -181
  43. data/spec/cases/graph_and_rest_api_spec.rb +0 -22
  44. data/spec/cases/graph_api_spec.rb +0 -22
  45. data/spec/cases/rest_api_spec.rb +0 -22
@@ -0,0 +1,41 @@
1
+ require 'faraday'
2
+
3
+ module Koala
4
+ module HTTPService
5
+ class MultipartRequest < Faraday::Request::Multipart
6
+ # Facebook expects nested parameters to be passed in a certain way
7
+ # Based on our testing (https://github.com/arsduo/koala/issues/125),
8
+ # Faraday needs two changes to make that work:
9
+ # 1) [] need to be escaped (e.g. params[foo]=bar ==> params%5Bfoo%5D=bar)
10
+ # 2) such messages need to be multipart-encoded
11
+
12
+ self.mime_type = 'multipart/form-data'.freeze
13
+
14
+ def process_request?(env)
15
+ # if the request values contain any hashes or arrays, multipart it
16
+ super || !!(env[:body].respond_to?(:values) && env[:body].values.find {|v| v.is_a?(Hash) || v.is_a?(Array)})
17
+ end
18
+
19
+
20
+ def process_params(params, prefix = nil, pieces = nil, &block)
21
+ params.inject(pieces || []) do |all, (key, value)|
22
+ key = "#{prefix}%5B#{key}%5D" if prefix
23
+
24
+ case value
25
+ when Array
26
+ values = value.inject([]) { |a,v| a << [nil, v] }
27
+ process_params(values, key, all, &block)
28
+ when Hash
29
+ process_params(value, key, all, &block)
30
+ else
31
+ all << block.call(key, value)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ # @private
39
+ # legacy support for when MultipartRequest lived directly under Koala
40
+ MultipartRequest = HTTPService::MultipartRequest
41
+ end
@@ -0,0 +1,18 @@
1
+ module Koala
2
+ module HTTPService
3
+ class Response
4
+ attr_reader :status, :body, :headers
5
+
6
+ # Creates a new Response object, which standardizes the response received by Facebook for use within Koala.
7
+ def initialize(status, body, headers)
8
+ @status = status
9
+ @body = body
10
+ @headers = headers
11
+ end
12
+ end
13
+ end
14
+
15
+ # @private
16
+ # legacy support for when Response lived directly under Koala
17
+ Response = HTTPService::Response
18
+ end
@@ -0,0 +1,187 @@
1
+ require "net/http/post/multipart"
2
+
3
+ module Koala
4
+ module HTTPService
5
+ class UploadableIO
6
+ attr_reader :io_or_path, :content_type, :filename
7
+
8
+ def initialize(io_or_path_or_mixed, content_type = nil, filename = nil)
9
+ # see if we got the right inputs
10
+ parse_init_mixed_param io_or_path_or_mixed, content_type
11
+
12
+ # filename is used in the Ads API
13
+ # if it's provided, take precedence over the detected filename
14
+ # otherwise, fall back to a dummy name
15
+ @filename = filename || @filename || "koala-io-file.dum"
16
+
17
+ raise KoalaError.new("Invalid arguments to initialize an UploadableIO") unless @io_or_path
18
+ raise KoalaError.new("Unable to determine MIME type for UploadableIO") if !@content_type
19
+ end
20
+
21
+ def to_upload_io
22
+ UploadIO.new(@io_or_path, @content_type, @filename)
23
+ end
24
+
25
+ def to_file
26
+ @io_or_path.is_a?(String) ? File.open(@io_or_path) : @io_or_path
27
+ end
28
+
29
+ def self.binary_content?(content)
30
+ content.is_a?(UploadableIO) || DETECTION_STRATEGIES.detect {|method| send(method, content)}
31
+ end
32
+
33
+ private
34
+ DETECTION_STRATEGIES = [
35
+ :sinatra_param?,
36
+ :rails_3_param?,
37
+ :file_param?
38
+ ]
39
+
40
+ PARSE_STRATEGIES = [
41
+ :parse_rails_3_param,
42
+ :parse_sinatra_param,
43
+ :parse_file_object,
44
+ :parse_string_path,
45
+ :parse_io
46
+ ]
47
+
48
+ def parse_init_mixed_param(mixed, content_type = nil)
49
+ PARSE_STRATEGIES.each do |method|
50
+ send(method, mixed, content_type)
51
+ return if @io_or_path && @content_type
52
+ end
53
+ end
54
+
55
+ # Expects a parameter of type ActionDispatch::Http::UploadedFile
56
+ def self.rails_3_param?(uploaded_file)
57
+ uploaded_file.respond_to?(:content_type) and uploaded_file.respond_to?(:tempfile) and uploaded_file.tempfile.respond_to?(:path)
58
+ end
59
+
60
+ def parse_rails_3_param(uploaded_file, content_type = nil)
61
+ if UploadableIO.rails_3_param?(uploaded_file)
62
+ @io_or_path = uploaded_file.tempfile.path
63
+ @content_type = content_type || uploaded_file.content_type
64
+ @filename = uploaded_file.original_filename
65
+ end
66
+ end
67
+
68
+ # Expects a Sinatra hash of file info
69
+ def self.sinatra_param?(file_hash)
70
+ file_hash.kind_of?(Hash) and file_hash.has_key?(:type) and file_hash.has_key?(:tempfile)
71
+ end
72
+
73
+ def parse_sinatra_param(file_hash, content_type = nil)
74
+ if UploadableIO.sinatra_param?(file_hash)
75
+ @io_or_path = file_hash[:tempfile]
76
+ @content_type = content_type || file_hash[:type] || detect_mime_type(tempfile)
77
+ @filename = file_hash[:filename]
78
+ end
79
+ end
80
+
81
+ # takes a file object
82
+ def self.file_param?(file)
83
+ file.kind_of?(File)
84
+ end
85
+
86
+ def parse_file_object(file, content_type = nil)
87
+ if UploadableIO.file_param?(file)
88
+ @io_or_path = file
89
+ @content_type = content_type || detect_mime_type(file.path)
90
+ @filename = File.basename(file.path)
91
+ end
92
+ end
93
+
94
+ def parse_string_path(path, content_type = nil)
95
+ if path.kind_of?(String)
96
+ @io_or_path = path
97
+ @content_type = content_type || detect_mime_type(path)
98
+ @filename = File.basename(path)
99
+ end
100
+ end
101
+
102
+ def parse_io(io, content_type = nil)
103
+ if io.respond_to?(:read)
104
+ @io_or_path = io
105
+ @content_type = content_type
106
+ end
107
+ end
108
+
109
+ MIME_TYPE_STRATEGIES = [
110
+ :use_mime_module,
111
+ :use_simple_detection
112
+ ]
113
+
114
+ def detect_mime_type(filename)
115
+ if filename
116
+ MIME_TYPE_STRATEGIES.each do |method|
117
+ result = send(method, filename)
118
+ return result if result
119
+ end
120
+ end
121
+ nil # if we can't find anything
122
+ end
123
+
124
+ def use_mime_module(filename)
125
+ # if the user has installed mime/types, we can use that
126
+ # if not, rescue and return nil
127
+ begin
128
+ type = MIME::Types.type_for(filename).first
129
+ type ? type.to_s : nil
130
+ rescue
131
+ nil
132
+ end
133
+ end
134
+
135
+ def use_simple_detection(filename)
136
+ # very rudimentary extension analysis for images
137
+ # first, get the downcased extension, or an empty string if it doesn't exist
138
+ extension = ((filename.match(/\.([a-zA-Z0-9]+)$/) || [])[1] || "").downcase
139
+ case extension
140
+ when ""
141
+ nil
142
+ # images
143
+ when "jpg", "jpeg"
144
+ "image/jpeg"
145
+ when "png"
146
+ "image/png"
147
+ when "gif"
148
+ "image/gif"
149
+
150
+ # video
151
+ when "3g2"
152
+ "video/3gpp2"
153
+ when "3gp", "3gpp"
154
+ "video/3gpp"
155
+ when "asf"
156
+ "video/x-ms-asf"
157
+ when "avi"
158
+ "video/x-msvideo"
159
+ when "flv"
160
+ "video/x-flv"
161
+ when "m4v"
162
+ "video/x-m4v"
163
+ when "mkv"
164
+ "video/x-matroska"
165
+ when "mod"
166
+ "video/mod"
167
+ when "mov", "qt"
168
+ "video/quicktime"
169
+ when "mp4", "mpeg4"
170
+ "video/mp4"
171
+ when "mpe", "mpeg", "mpg", "tod", "vob"
172
+ "video/mpeg"
173
+ when "nsv"
174
+ "application/x-winamp"
175
+ when "ogm", "ogv"
176
+ "video/ogg"
177
+ when "wmv"
178
+ "video/x-ms-wmv"
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ # @private
185
+ # legacy support for when UploadableIO lived directly under Koala
186
+ UploadableIO = HTTPService::UploadableIO
187
+ end
@@ -1,3 +1,7 @@
1
+ # OpenSSL and Base64 are required to support signed_request
2
+ require 'openssl'
3
+ require 'base64'
4
+
1
5
  module Koala
2
6
  module Facebook
3
7
 
@@ -5,21 +9,29 @@ module Koala
5
9
 
6
10
  class OAuth
7
11
  attr_reader :app_id, :app_secret, :oauth_callback_url
12
+
13
+ # Creates a new OAuth client.
14
+ #
15
+ # @param app_id [String, Integer] a Facebook application ID
16
+ # @param app_secret a Facebook application secret
17
+ # @param oauth_callback_url the URL in your app to which users authenticating with OAuth will be sent
8
18
  def initialize(app_id, app_secret, oauth_callback_url = nil)
9
19
  @app_id = app_id
10
20
  @app_secret = app_secret
11
21
  @oauth_callback_url = oauth_callback_url
12
22
  end
13
23
 
24
+ # Parses the cookie set Facebook's JavaScript SDK.
25
+ #
26
+ # @note in parsing Facebook's new signed cookie format this method has to make a request to Facebook.
27
+ # We recommend storing authenticated user info in your Rails session (or equivalent) and only
28
+ # calling this when needed.
29
+ #
30
+ # @param cookie_hash a set of cookies that includes the Facebook cookie.
31
+ # You can pass Rack/Rails/Sinatra's cookie hash directly to this method.
32
+ #
33
+ # @return the authenticated user's information as a hash, or nil.
14
34
  def get_user_info_from_cookie(cookie_hash)
15
- # Parses the cookie set Facebook's JavaScript SDK.
16
- # You can pass Rack/Rails/Sinatra's cookie hash directly to this method.
17
- #
18
- # If the user is logged in via Facebook, we return a dictionary with the
19
- # keys "uid" and "access_token". The former is the user's Facebook ID,
20
- # and the latter can be used to make authenticated requests to the Graph API.
21
- # If the user is not logged in, we return None.
22
-
23
35
  if signed_cookie = cookie_hash["fbsr_#{@app_id}"]
24
36
  parse_signed_cookie(signed_cookie)
25
37
  elsif unsigned_cookie = cookie_hash["fbs_#{@app_id}"]
@@ -28,6 +40,13 @@ module Koala
28
40
  end
29
41
  alias_method :get_user_info_from_cookies, :get_user_info_from_cookie
30
42
 
43
+ # Parses the cookie set Facebook's JavaScript SDK and returns only the user ID.
44
+ #
45
+ # @note (see #get_user_info_from_cookie)
46
+ #
47
+ # @param (see #get_user_info_from_cookie)
48
+ #
49
+ # @return the authenticated user's Facebook ID, or nil.
31
50
  def get_user_from_cookie(cookies)
32
51
  if info = get_user_info_from_cookies(cookies)
33
52
  # signed cookie has user_id, unsigned cookie has uid
@@ -38,6 +57,23 @@ module Koala
38
57
 
39
58
  # URLs
40
59
 
60
+ # Builds an OAuth URL, where users will be prompted to log in and for any desired permissions.
61
+ # When the users log in, you receive a callback with their
62
+ # See http://developers.facebook.com/docs/authentication/.
63
+ #
64
+ # @see #url_for_access_token
65
+ #
66
+ # @note The server-side authentication and dialog methods should only be used
67
+ # if your application can't use the Facebook Javascript SDK,
68
+ # which provides a much better user experience.
69
+ # See http://developers.facebook.com/docs/reference/javascript/.
70
+ #
71
+ # @param options any query values to add to the URL, as well as any special/required values listed below.
72
+ # @option options permissions an array or comma-separated string of desired permissions
73
+ #
74
+ # @raise ArgumentError if no OAuth callback was specified in OAuth#new or in options as :redirect_uri
75
+ #
76
+ # @return an OAuth URL you can send your users to
41
77
  def url_for_oauth_code(options = {})
42
78
  # for permissions, see http://developers.facebook.com/docs/authentication/permissions
43
79
  if permissions = options.delete(:permissions)
@@ -49,6 +85,20 @@ module Koala
49
85
  build_url("https://#{GRAPH_SERVER}/oauth/authorize", true, url_options)
50
86
  end
51
87
 
88
+ # Once you receive an OAuth code, you need to redeem it from Facebook using an appropriate URL.
89
+ # (This is done by your server behind the scenes.)
90
+ # See http://developers.facebook.com/docs/authentication/.
91
+ #
92
+ # @see #url_for_oauth_code
93
+ #
94
+ # @note (see #url_for_oauth_code)
95
+ #
96
+ # @param code an OAuth code received from Facebook
97
+ # @param options any additional query parameters to add to the URL
98
+ #
99
+ # @raise (see #url_for_oauth_code)
100
+ #
101
+ # @return an URL your server can query for the user's access token
52
102
  def url_for_access_token(code, options = {})
53
103
  # Creates the URL for the token corresponding to a given code generated by Facebook
54
104
  url_options = {
@@ -59,6 +109,15 @@ module Koala
59
109
  build_url("https://#{GRAPH_SERVER}/oauth/access_token", true, url_options)
60
110
  end
61
111
 
112
+ # Builds a URL for a given dialog (feed, friends, OAuth, pay, send, etc.)
113
+ # See http://developers.facebook.com/docs/reference/dialogs/.
114
+ #
115
+ # @note (see #url_for_oauth_code)
116
+ #
117
+ # @param dialog_type the kind of Facebook dialog you want to show
118
+ # @param options any additional query parameters to add to the URL
119
+ #
120
+ # @return an URL your server can query for the user's access token
62
121
  def url_for_dialog(dialog_type, options = {})
63
122
  # some endpoints require app_id, some client_id, supply both doesn't seem to hurt
64
123
  url_options = {:app_id => @app_id, :client_id => @app_id}.merge(options)
@@ -67,12 +126,36 @@ module Koala
67
126
 
68
127
  # access tokens
69
128
 
129
+ # Fetches an access token, token expiration, and other info from Facebook.
130
+ # Useful when you've received an OAuth code using the server-side authentication process.
131
+ # @see url_for_oauth_code
132
+ #
133
+ # @note (see #url_for_oauth_code)
134
+ #
135
+ # @param code (see #url_for_access_token)
136
+ # @param options any additional parameters to send to Facebook when redeeming the token
137
+ #
138
+ # @raise Koala::Facebook::APIError if Facebook returns an error response
139
+ #
140
+ # @return a hash of the access token info returned by Facebook (token, expiration, etc.)
70
141
  def get_access_token_info(code, options = {})
71
142
  # convenience method to get a parsed token from Facebook for a given code
72
143
  # should this require an OAuth callback URL?
73
144
  get_token_from_server({:code => code, :redirect_uri => options[:redirect_uri] || @oauth_callback_url}, false, options)
74
145
  end
75
146
 
147
+
148
+ # Fetches the access token (ignoring expiration and other info) from Facebook.
149
+ # Useful when you've received an OAuth code using the server-side authentication process.
150
+ # @see get_access_token_info
151
+ #
152
+ # @note (see #url_for_oauth_code)
153
+ #
154
+ # @param (see #get_access_token_info)
155
+ #
156
+ # @raise (see #get_access_token_info)
157
+ #
158
+ # @return the access token
76
159
  def get_access_token(code, options = {})
77
160
  # upstream methods will throw errors if needed
78
161
  if info = get_access_token_info(code, options)
@@ -80,22 +163,36 @@ module Koala
80
163
  end
81
164
  end
82
165
 
166
+ # Fetches the application's access token, along with any other information provided by Facebook.
167
+ # See http://developers.facebook.com/docs/authentication/ (search for App Login).
168
+ #
169
+ # @param options any additional parameters to send to Facebook when redeeming the token
170
+ #
171
+ # @return the application access token and other information (expiration, etc.)
83
172
  def get_app_access_token_info(options = {})
84
173
  # convenience method to get a the application's sessionless access token
85
174
  get_token_from_server({:type => 'client_cred'}, true, options)
86
175
  end
87
176
 
177
+ # Fetches the application's access token (ignoring expiration and other info).
178
+ # @see get_app_access_token_info
179
+ #
180
+ # @param (see #get_app_access_token_info)
181
+ #
182
+ # @return the application access token
88
183
  def get_app_access_token(options = {})
89
184
  if info = get_app_access_token_info(options)
90
185
  string = info["access_token"]
91
186
  end
92
187
  end
93
188
 
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.
189
+ # Parses a signed request string provided by Facebook to canvas apps or in a secure cookie.
190
+ #
191
+ # @param input the signed request from Facebook
192
+ #
193
+ # @raise RuntimeError if the signature is incomplete, invalid, or using an unsupported algorithm
194
+ #
195
+ # @return a hash of the validated request information
99
196
  def parse_signed_request(input)
100
197
  encoded_sig, encoded_envelope = input.split('.', 2)
101
198
  raise 'SignedRequest: Invalid (incomplete) signature data' unless encoded_sig && encoded_envelope
@@ -112,8 +209,12 @@ module Koala
112
209
  envelope
113
210
  end
114
211
 
115
- # from session keys
212
+ # Old session key code
213
+
214
+ # @deprecated Facebook no longer provides session keys.
116
215
  def get_token_info_from_session_keys(sessions, options = {})
216
+ Koala::Utils.deprecate("Facebook no longer provides session keys. The relevant OAuth methods will be removed in the next release.")
217
+
117
218
  # fetch the OAuth tokens from Facebook
118
219
  response = fetch_token_string({
119
220
  :type => 'client_cred',
@@ -131,6 +232,7 @@ module Koala
131
232
  MultiJson.decode(response)
132
233
  end
133
234
 
235
+ # @deprecated (see #get_token_info_from_session_keys)
134
236
  def get_tokens_from_session_keys(sessions, options = {})
135
237
  # get the original hash results
136
238
  results = get_token_info_from_session_keys(sessions, options)
@@ -138,6 +240,7 @@ module Koala
138
240
  results.collect { |r| r ? r["access_token"] : nil }
139
241
  end
140
242
 
243
+ # @deprecated (see #get_token_info_from_session_keys)
141
244
  def get_token_from_session_key(session, options = {})
142
245
  # convenience method for a single key
143
246
  # gets the overlaoded strings automatically