koala 1.6.0 → 1.7.0rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -41,8 +41,13 @@ module Koala
41
41
  #
42
42
  # @return the body of the response from Facebook (unless another http_component is requested)
43
43
  def api(path, args = {}, verb = "get", options = {}, &error_checking_block)
44
- # Fetches the given path in the Graph API.
45
- args["access_token"] = @access_token || @app_access_token if @access_token || @app_access_token
44
+ # If a access token is explicitly provided, use that
45
+ # This is explicitly needed in batch requests so GraphCollection
46
+ # results preserve any specific access tokens provided
47
+ args["access_token"] ||= @access_token || @app_access_token if @access_token || @app_access_token
48
+
49
+ # Translate any arrays in the params into comma-separated strings
50
+ args = sanitize_request_parameters(args)
46
51
 
47
52
  # add a leading /
48
53
  path = "/#{path}" unless path =~ /^\//
@@ -53,7 +58,7 @@ module Koala
53
58
  if result.status.to_i >= 500
54
59
  raise Koala::Facebook::ServerError.new(result.status.to_i, result.body)
55
60
  end
56
-
61
+
57
62
  yield result if error_checking_block
58
63
 
59
64
  # if we want a component other than the body (e.g. redirect header for images), return that
@@ -66,6 +71,26 @@ module Koala
66
71
  MultiJson.load("[#{result.body.to_s}]")[0]
67
72
  end
68
73
  end
74
+
75
+ private
76
+
77
+ # Sanitizes Ruby objects into Facebook-compatible string values.
78
+ #
79
+ # @param parameters a hash of parameters.
80
+ #
81
+ # Returns a hash in which values that are arrays of non-enumerable values
82
+ # (Strings, Symbols, Numbers, etc.) are turned into comma-separated strings.
83
+ def sanitize_request_parameters(parameters)
84
+ parameters.reduce({}) do |result, (key, value)|
85
+ # if the parameter is an array that contains non-enumerable values,
86
+ # turn it into a comma-separated list
87
+ # in Ruby 1.8.7, strings are enumerable, but we don't care
88
+ if value.is_a?(Array) && value.none? {|entry| entry.is_a?(Enumerable) && !entry.is_a?(String)}
89
+ value = value.join(",")
90
+ end
91
+ result.merge(key => value)
92
+ end
93
+ end
69
94
  end
70
95
  end
71
96
  end
@@ -5,8 +5,6 @@ require 'koala/http_service/uploadable_io'
5
5
 
6
6
  module Koala
7
7
  module Facebook
8
- GRAPH_SERVER = "graph.facebook.com"
9
-
10
8
  # Methods used to interact with the Facebook Graph API.
11
9
  #
12
10
  # See https://github.com/arsduo/koala/wiki/Graph-API for a general introduction to Koala
@@ -144,6 +142,7 @@ module Koala
144
142
  def put_connections(id, connection_name, args = {}, options = {}, &block)
145
143
  # Posts a certain connection
146
144
  raise AuthenticationError.new(nil, nil, "Write operations require an access token") unless @access_token
145
+
147
146
  graph_call("#{id}/#{connection_name}", args, "post", options, &block)
148
147
  end
149
148
 
@@ -178,7 +177,7 @@ module Koala
178
177
  def get_picture(object, args = {}, options = {}, &block)
179
178
  # Gets a picture object, returning the URL (which Facebook sends as a header)
180
179
  resolved_result = graph_call("#{object}/picture", args, "get", options.merge(:http_component => :headers)) do |result|
181
- result["Location"]
180
+ result ? result["Location"] : nil
182
181
  end
183
182
  block ? block.call(resolved_result) : resolved_result
184
183
  end
@@ -228,6 +227,9 @@ module Koala
228
227
  # @param message the message to write for the wall
229
228
  # @param attachment a hash describing the wall post
230
229
  # (see the {https://developers.facebook.com/docs/guides/attachments/ stream attachments} documentation.)
230
+ # If attachment contains a properties key, this will be turned to
231
+ # JSON (if it's a hash) since Facebook's API, oddly, requires
232
+ # this.
231
233
  # @param target_id the target wall
232
234
  # @param options (see #get_object)
233
235
  # @param block (see Koala::Facebook::API#api)
@@ -244,6 +246,10 @@ module Koala
244
246
  # @see #put_connections
245
247
  # @return (see #put_connections)
246
248
  def put_wall_post(message, attachment = {}, target_id = "me", options = {}, &block)
249
+ if properties = attachment.delete(:properties) || attachment.delete("properties")
250
+ properties = MultiJson.dump(properties) if properties.is_a?(Hash) || properties.is_a?(Array)
251
+ attachment["properties"] = properties
252
+ end
247
253
  put_connections(target_id, "feed", attachment.merge({:message => message}), options, &block)
248
254
  end
249
255
 
@@ -368,6 +374,21 @@ module Koala
368
374
  block ? block.call(access_token) : access_token
369
375
  end
370
376
 
377
+ # Get an access token information
378
+ # The access token used to instantiate the API object needs to be
379
+ # the app access token or a valid User Access Token from a developer of the app.
380
+ # See https://developers.facebook.com/docs/howtos/login/debugging-access-tokens/#step1
381
+ #
382
+ # @param input_token the access token you want to inspect
383
+ # @param block (see Koala::Facebook::API#api)
384
+ #
385
+ # @return a JSON array containing data and a map of fields
386
+ def debug_token(input_token, &block)
387
+ access_token_info = graph_call("debug_token", {:input_token => input_token})
388
+
389
+ block ? block.call(access_token_info) : access_token_info
390
+ end
391
+
371
392
  # Fetches the comments from fb:comments widgets for a given set of URLs (array or comma-separated string).
372
393
  # See https://developers.facebook.com/blog/post/490.
373
394
  #
@@ -383,6 +404,15 @@ module Koala
383
404
  get_object("comments", args, options, &block)
384
405
  end
385
406
 
407
+ # App restrictions require you to JSON-encode the restriction value. This
408
+ # is neither obvious nor intuitive, so this convenience method is
409
+ # provided.
410
+ #
411
+ # @params app_id the application to apply the restrictions to
412
+ # @params restrictions_hash the restrictions to apply
413
+ # @param args (see #get_object)
414
+ # @param options (see #get_object)
415
+ # @param block (see Koala::Facebook::API#api)
386
416
  def set_app_restrictions(app_id, restrictions_hash, args = {}, options = {}, &block)
387
417
  graph_call(app_id, args.merge(:restrictions => MultiJson.dump(restrictions_hash)), "post", options, &block)
388
418
  end
@@ -6,22 +6,22 @@ module Koala
6
6
  # A light wrapper for collections returned from the Graph API.
7
7
  # It extends Array to allow you to page backward and forward through
8
8
  # result sets, and providing easy access to paging information.
9
- class GraphCollection < Array
10
-
9
+ class GraphCollection < Array
10
+
11
11
  # The raw paging information from Facebook (next/previous URLs).
12
12
  attr_reader :paging
13
13
  # @return [Koala::Facebook::GraphAPI] the api used to make requests.
14
14
  attr_reader :api
15
15
  # The entire raw response from Facebook.
16
16
  attr_reader :raw_response
17
-
17
+
18
18
  # Initialize the array of results and store various additional paging-related information.
19
- #
19
+ #
20
20
  # @param response the response from Facebook (a hash whose "data" key is an array)
21
21
  # @param api the Graph {Koala::Facebook::API API} instance to use to make calls
22
22
  # (usually the API that made the original call).
23
23
  #
24
- # @return [Koala::Facebook::GraphCollection] an initialized GraphCollection
24
+ # @return [Koala::Facebook::GraphCollection] an initialized GraphCollection
25
25
  # whose paging, raw_response, and api attributes are populated.
26
26
  def initialize(response, api)
27
27
  super response["data"]
@@ -31,32 +31,32 @@ module Koala
31
31
  end
32
32
 
33
33
  # @private
34
- # Turn the response into a GraphCollection if they're pageable;
34
+ # Turn the response into a GraphCollection if they're pageable;
35
35
  # if not, return the original response.
36
36
  # The Ads API (uniquely so far) returns a hash rather than an array when queried
37
- # with get_connections.
37
+ # with get_connections.
38
38
  def self.evaluate(response, api)
39
39
  response.is_a?(Hash) && response["data"].is_a?(Array) ? self.new(response, api) : response
40
40
  end
41
-
41
+
42
42
  # Retrieve the next page of results.
43
- #
43
+ #
44
44
  # @return a GraphCollection array of additional results (an empty array if there are no more results)
45
45
  def next_page
46
46
  base, args = next_page_params
47
47
  base ? @api.get_page([base, args]) : nil
48
48
  end
49
-
49
+
50
50
  # Retrieve the previous page of results.
51
- #
51
+ #
52
52
  # @return a GraphCollection array of additional results (an empty array if there are no earlier results)
53
53
  def previous_page
54
54
  base, args = previous_page_params
55
55
  base ? @api.get_page([base, args]) : nil
56
56
  end
57
-
58
- # Arguments that can be sent to {Koala::Facebook::API#graph_call} to retrieve the next page of results.
59
- #
57
+
58
+ # Arguments that can be sent to {Koala::Facebook::API#graph_call} to retrieve the next page of results.
59
+ #
60
60
  # @example
61
61
  # @api.graph_call(*collection.next_page_params)
62
62
  #
@@ -64,9 +64,9 @@ module Koala
64
64
  def next_page_params
65
65
  @paging && @paging["next"] ? parse_page_url(@paging["next"]) : nil
66
66
  end
67
-
68
- # Arguments that can be sent to {Koala::Facebook::API#graph_call} to retrieve the previous page of results.
69
- #
67
+
68
+ # Arguments that can be sent to {Koala::Facebook::API#graph_call} to retrieve the previous page of results.
69
+ #
70
70
  # @example
71
71
  # @api.graph_call(*collection.previous_page_params)
72
72
  #
@@ -74,7 +74,7 @@ module Koala
74
74
  def previous_page_params
75
75
  @paging && @paging["previous"] ? parse_page_url(@paging["previous"]) : nil
76
76
  end
77
-
77
+
78
78
  # @private
79
79
  def parse_page_url(url)
80
80
  GraphCollection.parse_page_url(url)
@@ -102,9 +102,9 @@ module Koala
102
102
  end
103
103
  end
104
104
  end
105
-
105
+
106
106
  # @private
107
- # legacy support for when GraphCollection lived directly under Koala::Facebook
107
+ # legacy support for when GraphCollection lived directly under Koala::Facebook
108
108
  GraphCollection = API::GraphCollection
109
109
  end
110
110
  end
@@ -1,8 +1,6 @@
1
1
  module Koala
2
2
  module Facebook
3
- REST_SERVER = "api.facebook.com"
4
-
5
- # Methods used to interact with Facebook's legacy REST API.
3
+ # Methods used to interact with Facebook's legacy REST API.
6
4
  # Where possible, you should use the newer, faster Graph API to interact with Facebook;
7
5
  # in the future, the REST API will be deprecated.
8
6
  # For now, though, there are a few methods that can't be done through the Graph API.
@@ -23,6 +23,23 @@ module Koala
23
23
  builder.adapter Faraday.default_adapter
24
24
  end
25
25
 
26
+ # Default servers for Facebook. These are read into the config OpenStruct,
27
+ # and can be overridden via Koala.config.
28
+ DEFAULT_SERVERS = {
29
+ :graph_server => 'graph.facebook.com',
30
+ :dialog_host => 'www.facebook.com',
31
+ :rest_server => 'api.facebook.com',
32
+ # certain Facebook services (beta, video) require you to access different
33
+ # servers. If you're using your own servers, for instance, for a proxy,
34
+ # you can change both the matcher and the replacement values.
35
+ # So for instance, if you're talking to fbproxy.mycompany.com, you could
36
+ # set up beta.fbproxy.mycompany.com for FB's beta tier, and set the
37
+ # matcher to /\.fbproxy/ and the beta_replace to '.beta.fbproxy'.
38
+ :host_path_matcher => /\.facebook/,
39
+ :video_replace => '-video.facebook',
40
+ :beta_replace => '.beta.facebook'
41
+ }
42
+
26
43
  # The address of the appropriate Facebook server.
27
44
  #
28
45
  # @param options various flags to indicate which server to use.
@@ -33,9 +50,9 @@ module Koala
33
50
  #
34
51
  # @return a complete server address with protocol
35
52
  def self.server(options = {})
36
- server = "#{options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER}"
37
- server.gsub!(/\.facebook/, "-video.facebook") if options[:video]
38
- server.gsub!(/\.facebook/, ".beta.facebook") if options[:beta]
53
+ server = "#{options[:rest_api] ? Koala.config.rest_server : Koala.config.graph_server}"
54
+ server.gsub!(Koala.config.host_path_matcher, Koala.config.video_replace) if options[:video]
55
+ server.gsub!(Koala.config.host_path_matcher, Koala.config.beta_replace) if options[:beta]
39
56
  "#{options[:use_ssl] ? "https" : "http"}://#{server}"
40
57
  end
41
58
 
@@ -126,18 +143,6 @@ module Koala
126
143
  http_options[:timeout] = value
127
144
  end
128
145
 
129
- # @private
130
- def self.timeout
131
- Koala::Utils.deprecate("HTTPService.timeout is now HTTPService.http_options[:timeout]; .timeout will be removed in a future version.")
132
- http_options[:timeout]
133
- end
134
-
135
- # @private
136
- def self.timeout=(value)
137
- Koala::Utils.deprecate("HTTPService.timeout is now HTTPService.http_options[:timeout]; .timeout will be removed in a future version.")
138
- http_options[:timeout] = value
139
- end
140
-
141
146
  # @private
142
147
  def self.proxy
143
148
  Koala::Utils.deprecate("HTTPService.proxy is now HTTPService.http_options[:proxy]; .proxy will be removed in a future version.")
@@ -230,4 +235,4 @@ module Koala
230
235
  Faraday.default_adapter = :net_http
231
236
  end
232
237
  end
233
- end
238
+ end
@@ -4,9 +4,6 @@ require 'base64'
4
4
 
5
5
  module Koala
6
6
  module Facebook
7
-
8
- DIALOG_HOST = "www.facebook.com"
9
-
10
7
  class OAuth
11
8
  attr_reader :app_id, :app_secret, :oauth_callback_url
12
9
 
@@ -23,9 +20,12 @@ module Koala
23
20
 
24
21
  # Parses the cookie set Facebook's JavaScript SDK.
25
22
  #
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.
23
+ # @note this method can only be called once per session, as the OAuth code
24
+ # Facebook supplies can only be redeemed once. Your application
25
+ # must handle cross-request storage of this information; you can no
26
+ # longer call this method multiple times. (This works out, as the
27
+ # method has to make a call to FB's servers anyway, which you don't
28
+ # want on every call.)
29
29
  #
30
30
  # @param cookie_hash a set of cookies that includes the Facebook cookie.
31
31
  # You can pass Rack/Rails/Sinatra's cookie hash directly to this method.
@@ -48,6 +48,7 @@ module Koala
48
48
  #
49
49
  # @return the authenticated user's Facebook ID, or nil.
50
50
  def get_user_from_cookies(cookies)
51
+ Koala::Utils.deprecate("Due to Facebook changes, you can only redeem an OAuth code once; it is therefore recommended not to use this method, as it will consume the code without providing you the access token. See https://developers.facebook.com/roadmap/completed-changes/#december-2012.")
51
52
  if signed_cookie = cookies["fbsr_#{@app_id}"]
52
53
  if components = parse_signed_request(signed_cookie)
53
54
  components["user_id"]
@@ -74,6 +75,9 @@ module Koala
74
75
  #
75
76
  # @param options any query values to add to the URL, as well as any special/required values listed below.
76
77
  # @option options permissions an array or comma-separated string of desired permissions
78
+ # @option options state a unique string to serve as a CSRF (cross-site request
79
+ # forgery) token -- highly recommended for security. See
80
+ # https://developers.facebook.com/docs/howtos/login/server-side-login/
77
81
  #
78
82
  # @raise ArgumentError if no OAuth callback was specified in OAuth#new or in options as :redirect_uri
79
83
  #
@@ -86,7 +90,7 @@ module Koala
86
90
  url_options = {:client_id => @app_id}.merge(options)
87
91
 
88
92
  # Creates the URL for oauth authorization for a given callback and optional set of permissions
89
- build_url("https://#{GRAPH_SERVER}/oauth/authorize", true, url_options)
93
+ build_url("https://#{Koala.config.dialog_host}/dialog/oauth", true, url_options)
90
94
  end
91
95
 
92
96
  # Once you receive an OAuth code, you need to redeem it from Facebook using an appropriate URL.
@@ -110,7 +114,7 @@ module Koala
110
114
  :code => code,
111
115
  :client_secret => @app_secret
112
116
  }.merge(options)
113
- build_url("https://#{GRAPH_SERVER}/oauth/access_token", true, url_options)
117
+ build_url("https://#{Koala.config.graph_server}/oauth/access_token", true, url_options)
114
118
  end
115
119
 
116
120
  # Builds a URL for a given dialog (feed, friends, OAuth, pay, send, etc.)
@@ -125,7 +129,7 @@ module Koala
125
129
  def url_for_dialog(dialog_type, options = {})
126
130
  # some endpoints require app_id, some client_id, supply both doesn't seem to hurt
127
131
  url_options = {:app_id => @app_id, :client_id => @app_id}.merge(options)
128
- build_url("http://#{DIALOG_HOST}/dialog/#{dialog_type}", true, url_options)
132
+ build_url("http://#{Koala.config.dialog_host}/dialog/#{dialog_type}", true, url_options)
129
133
  end
130
134
 
131
135
  # access tokens
@@ -175,7 +179,7 @@ module Koala
175
179
  # @return the application access token and other information (expiration, etc.)
176
180
  def get_app_access_token_info(options = {})
177
181
  # convenience method to get a the application's sessionless access token
178
- get_token_from_server({:type => 'client_cred'}, true, options)
182
+ get_token_from_server({:grant_type => 'client_credentials'}, true, options)
179
183
  end
180
184
 
181
185
  # Fetches the application's access token (ignoring expiration and other info).
@@ -346,7 +350,7 @@ module Koala
346
350
 
347
351
  def build_url(base, require_redirect_uri = false, url_options = {})
348
352
  if require_redirect_uri && !(url_options[:redirect_uri] ||= url_options.delete(:callback) || @oauth_callback_url)
349
- raise ArgumentError, "url_for_dialog must get a callback either from the OAuth object or in the parameters!"
353
+ raise ArgumentError, "build_url must get a callback either from the OAuth object or in the parameters!"
350
354
  end
351
355
 
352
356
  "#{base}?#{Koala::HTTPService.encode_params(url_options)}"
@@ -1,3 +1,3 @@
1
1
  module Koala
2
- VERSION = "1.6.0"
2
+ VERSION = "1.7.0rc1"
3
3
  end
data/readme.md CHANGED
@@ -1,5 +1,10 @@
1
1
  [![Build Status](https://secure.travis-ci.org/arsduo/koala.png)](http://travis-ci.org/arsduo/koala)
2
2
 
3
+ **Note**: a recent Facebook change will cause apps that parse the cookies every
4
+ request to fail with the error "OAuthException: This authorization code has
5
+ been used." If you're seeing this, please read the note in the [OAuth
6
+ wiki](https://github.com/arsduo/koala/wiki/OAuth) for more information.
7
+
3
8
  Koala
4
9
  ====
5
10
  [Koala](http://github.com/arsduo/koala) is a Facebook library for Ruby, supporting the Graph API (including the batch requests and photo uploads), the REST API, realtime updates, test users, and OAuth validation. We wrote Koala with four goals:
@@ -7,7 +12,7 @@ Koala
7
12
  * Lightweight: Koala should be as light and simple as Facebook’s own libraries, providing API accessors and returning simple JSON.
8
13
  * Fast: Koala should, out of the box, be quick. Out of the box, we use Facebook's faster read-only servers when possible and if available, the Typhoeus gem to make snappy Facebook requests. Of course, that brings us to our next topic:
9
14
  * Flexible: Koala should be useful to everyone, regardless of their current configuration. We support JRuby, Rubinius, and REE as well as vanilla Ruby (1.8.7, 1.9.2, and 1.9.3), and use the Faraday library to provide complete flexibility over how HTTP requests are made.
10
- * Tested: Koala should have complete test coverage, so you can rely on it. Our test coverage is complete and can be run against either mocked responses or the live Facebook servers; we're also on [Travis CI](travis-ci.org/arsduo/koala/).
15
+ * Tested: Koala should have complete test coverage, so you can rely on it. Our test coverage is complete and can be run against either mocked responses or the live Facebook servers; we're also on [Travis CI](http://travis-ci.org/arsduo/koala/).
11
16
 
12
17
  Installation
13
18
  ---
@@ -24,9 +29,9 @@ gem "koala"
24
29
 
25
30
  Graph API
26
31
  ----
27
- The Graph API is the simple, slick new interface to Facebook's data.
28
- Using it with Koala is quite straightforward. First, you'll need an access token, which you can get through
29
- Facebook's [Graph API Explorer](https://developers.facebook.com/tools/explorer) (click on 'Get Access Token').
32
+ The Graph API is the simple, slick new interface to Facebook's data.
33
+ Using it with Koala is quite straightforward. First, you'll need an access token, which you can get through
34
+ Facebook's [Graph API Explorer](https://developers.facebook.com/tools/explorer) (click on 'Get Access Token').
30
35
  Then, go exploring:
31
36
 
32
37
  ```ruby
@@ -110,6 +115,23 @@ fql = @api.fql_query(my_fql_query)
110
115
  @api.put_wall_post(process_result(fql))
111
116
  ```
112
117
 
118
+ Configuration
119
+ ----
120
+ You can change the host that koala makes requests to (point to a mock server, apigee, runscope etc..)
121
+ ```ruby
122
+ # config/initializers/koala.rb
123
+ require 'koala'
124
+
125
+ Koala.configure do |config|
126
+ config.graph_server = 'my-graph-mock.mysite.com'
127
+ # other common options are `rest_server` and `dialog_host`
128
+ # see lib/koala/http_service.rb
129
+ end
130
+ ```
131
+
132
+ Of course the defaults are the facebook endpoints and you can additionally configure the beta
133
+ tier and video upload matching and replacement strings.
134
+
113
135
  OAuth
114
136
  -----
115
137
  You can use the Graph and REST APIs without an OAuth access token, but the real magic happens when you provide Facebook an OAuth token to prove you're authenticated. Koala provides an OAuth class to make that process easy:
@@ -119,10 +141,15 @@ You can use the Graph and REST APIs without an OAuth access token, but the real
119
141
 
120
142
  If your application uses Koala and the Facebook [JavaScript SDK](http://github.com/facebook/facebook-js-sdk) (formerly Facebook Connect), you can use the OAuth class to parse the cookies:
121
143
  ```ruby
122
- @oauth.get_user_from_cookies(cookies) # gets the user's ID
123
- @oauth.get_user_info_from_cookies(cookies) # parses and returns the entire hash
144
+ # parses and returns a hash including the token and the user id
145
+ # NOTE: this method can only be called once per session, as the OAuth code
146
+ # Facebook supplies can only be redeemed once. Your application must handle
147
+ # cross-request storage of this information; you can no longer call this method
148
+ # multiple times.
149
+ @oauth.get_user_info_from_cookies(cookies)
124
150
  ```
125
151
  And if you have to use the more complicated [redirect-based OAuth process](http://developers.facebook.com/docs/authentication/), Koala helps out there, too:
152
+
126
153
  ```ruby
127
154
  # generate authenticating URL
128
155
  @oauth.url_for_oauth_code
@@ -223,4 +250,4 @@ LIVE=true bundle exec rake spec
223
250
  # you can also test against Facebook's beta tier
224
251
  LIVE=true BETA=true bundle exec rake spec
225
252
  ```
226
- By default, the live tests are run against test users, so you can run them as frequently as you want. If you want to run them against a real user, however, you can fill in the OAuth token, code, and access\_token values in spec/fixtures/facebook_data.yml. See the wiki for more details.
253
+ By default, the live tests are run against test users, so you can run them as frequently as you want. If you want to run them against a real user, however, you can fill in the OAuth token, code, and access\_token values in spec/fixtures/facebook_data.yml. See the wiki for more details.