rest-graph 1.7.0 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/.gitignore +6 -0
  2. data/CHANGES +44 -0
  3. data/CONTRIBUTORS +1 -0
  4. data/README +221 -191
  5. data/README.md +367 -0
  6. data/Rakefile +28 -43
  7. data/TODO +1 -0
  8. data/doc/ToC.md +10 -0
  9. data/doc/dependency.md +78 -0
  10. data/doc/design.md +206 -0
  11. data/doc/rails.md +12 -0
  12. data/doc/test.md +46 -0
  13. data/doc/tutorial.md +142 -0
  14. data/example/rails2/Gemfile +13 -0
  15. data/example/rails2/app/controllers/application_controller.rb +10 -8
  16. data/example/rails2/app/views/application/helper.html.erb +1 -0
  17. data/example/rails2/config/boot.rb +16 -0
  18. data/example/rails2/config/environment.rb +3 -30
  19. data/example/rails2/config/preinitializer.rb +23 -0
  20. data/example/rails2/test/functional/application_controller_test.rb +72 -32
  21. data/example/rails2/test/test_helper.rb +10 -6
  22. data/example/rails3/Gemfile +13 -0
  23. data/example/rails3/Rakefile +7 -0
  24. data/example/rails3/app/controllers/application_controller.rb +118 -0
  25. data/example/rails3/app/views/application/helper.html.erb +1 -0
  26. data/example/rails3/config.ru +4 -0
  27. data/example/rails3/config/application.rb +23 -0
  28. data/example/rails3/config/environment.rb +5 -0
  29. data/example/rails3/config/environments/development.rb +26 -0
  30. data/example/rails3/config/environments/production.rb +49 -0
  31. data/example/rails3/config/environments/test.rb +30 -0
  32. data/example/rails3/config/initializers/secret_token.rb +7 -0
  33. data/example/rails3/config/initializers/session_store.rb +8 -0
  34. data/example/rails3/config/rest-graph.yaml +11 -0
  35. data/example/rails3/config/routes.rb +5 -0
  36. data/example/rails3/test/functional/application_controller_test.rb +183 -0
  37. data/example/rails3/test/test_helper.rb +18 -0
  38. data/example/rails3/test/unit/rails_util_test.rb +44 -0
  39. data/init.rb +1 -1
  40. data/lib/rest-graph.rb +5 -571
  41. data/lib/rest-graph/auto_load.rb +3 -3
  42. data/lib/rest-graph/autoload.rb +3 -3
  43. data/lib/rest-graph/config_util.rb +43 -0
  44. data/lib/rest-graph/core.rb +608 -0
  45. data/lib/rest-graph/facebook_util.rb +74 -0
  46. data/lib/rest-graph/rails_util.rb +85 -37
  47. data/lib/rest-graph/test_util.rb +18 -2
  48. data/lib/rest-graph/version.rb +2 -2
  49. data/rest-graph.gemspec +42 -47
  50. data/task/gemgem.rb +155 -0
  51. data/test/test_api.rb +16 -0
  52. data/test/test_cache.rb +28 -8
  53. data/test/test_error.rb +9 -0
  54. data/test/test_facebook.rb +36 -0
  55. data/test/test_load_config.rb +16 -14
  56. data/test/test_misc.rb +4 -4
  57. data/test/test_parse.rb +10 -4
  58. metadata +146 -186
  59. data/Gemfile.lock +0 -45
  60. data/README.rdoc +0 -337
  61. data/example/rails2/script/console +0 -3
  62. data/example/rails2/script/server +0 -3
  63. data/lib/rest-graph/load_config.rb +0 -41
@@ -0,0 +1 @@
1
+ <%= rest_graph.app_id %>
@@ -0,0 +1,4 @@
1
+ # This file is used by Rack-based servers to start the application.
2
+
3
+ require ::File.expand_path('../config/environment', __FILE__)
4
+ run Rails3::Application
@@ -0,0 +1,23 @@
1
+
2
+ # Set up gems listed in the Gemfile.
3
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
4
+
5
+ require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
6
+
7
+ require 'action_controller/railtie'
8
+ require 'rails/test_unit/railtie'
9
+
10
+ Bundler.require(:default, Rails.env)
11
+
12
+ module Rails3
13
+ class Application < Rails::Application
14
+ config.encoding = 'utf-8'
15
+
16
+ logger = Logger.new($stdout)
17
+ logger.level = Logger::INFO
18
+ config.logger = logger
19
+
20
+ # Configure sensitive parameters which will be filtered from the log file.
21
+ config.filter_parameters += [:password]
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ # Load the rails application
2
+ require File.expand_path('../application', __FILE__)
3
+
4
+ # Initialize the rails application
5
+ Rails3::Application.initialize!
@@ -0,0 +1,26 @@
1
+ Rails3::Application.configure do
2
+ # Settings specified here will take precedence over those in config/application.rb
3
+
4
+ # In the development environment your application's code is reloaded on
5
+ # every request. This slows down response time but is perfect for development
6
+ # since you don't have to restart the webserver when you make code changes.
7
+ config.cache_classes = false
8
+
9
+ # Log error messages when you accidentally call methods on nil.
10
+ config.whiny_nils = true
11
+
12
+ # Show full error reports and disable caching
13
+ config.consider_all_requests_local = true
14
+ config.action_view.debug_rjs = true
15
+ config.action_controller.perform_caching = false
16
+
17
+ # Don't care if the mailer can't send
18
+ config.action_mailer.raise_delivery_errors = false
19
+
20
+ # Print deprecation notices to the Rails logger
21
+ config.active_support.deprecation = :log
22
+
23
+ # Only use best-standards-support built into browsers
24
+ config.action_dispatch.best_standards_support = :builtin
25
+ end
26
+
@@ -0,0 +1,49 @@
1
+ Rails3::Application.configure do
2
+ # Settings specified here will take precedence over those in config/application.rb
3
+
4
+ # The production environment is meant for finished, "live" apps.
5
+ # Code is not reloaded between requests
6
+ config.cache_classes = true
7
+
8
+ # Full error reports are disabled and caching is turned on
9
+ config.consider_all_requests_local = false
10
+ config.action_controller.perform_caching = true
11
+
12
+ # Specifies the header that your server uses for sending files
13
+ config.action_dispatch.x_sendfile_header = "X-Sendfile"
14
+
15
+ # For nginx:
16
+ # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'
17
+
18
+ # If you have no front-end server that supports something like X-Sendfile,
19
+ # just comment this out and Rails will serve the files
20
+
21
+ # See everything in the log (default is :info)
22
+ # config.log_level = :debug
23
+
24
+ # Use a different logger for distributed setups
25
+ # config.logger = SyslogLogger.new
26
+
27
+ # Use a different cache store in production
28
+ # config.cache_store = :mem_cache_store
29
+
30
+ # Disable Rails's static asset server
31
+ # In production, Apache or nginx will already do this
32
+ config.serve_static_assets = false
33
+
34
+ # Enable serving of images, stylesheets, and javascripts from an asset server
35
+ # config.action_controller.asset_host = "http://assets.example.com"
36
+
37
+ # Disable delivery errors, bad email addresses will be ignored
38
+ # config.action_mailer.raise_delivery_errors = false
39
+
40
+ # Enable threaded mode
41
+ # config.threadsafe!
42
+
43
+ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
44
+ # the I18n.default_locale when a translation can not be found)
45
+ config.i18n.fallbacks = true
46
+
47
+ # Send deprecation notices to registered listeners
48
+ config.active_support.deprecation = :notify
49
+ end
@@ -0,0 +1,30 @@
1
+ Rails3::Application.configure do
2
+ # Settings specified here will take precedence over those in config/application.rb
3
+
4
+ # The test environment is used exclusively to run your application's
5
+ # test suite. You never need to work with it otherwise. Remember that
6
+ # your test database is "scratch space" for the test suite and is wiped
7
+ # and recreated between test runs. Don't rely on the data there!
8
+ config.cache_classes = true
9
+
10
+ # Log error messages when you accidentally call methods on nil.
11
+ config.whiny_nils = true
12
+
13
+ # Show full error reports and disable caching
14
+ config.consider_all_requests_local = true
15
+ config.action_controller.perform_caching = false
16
+
17
+ # Raise exceptions instead of rendering exception templates
18
+ config.action_dispatch.show_exceptions = false
19
+
20
+ # Disable request forgery protection in test environment
21
+ config.action_controller.allow_forgery_protection = false
22
+
23
+ # Use SQL instead of Active Record's schema dumper when creating the test database.
24
+ # This is necessary if your schema can't be completely dumped by the schema dumper,
25
+ # like if you have constraints or database-specific column types
26
+ # config.active_record.schema_format = :sql
27
+
28
+ # Print deprecation notices to the stderr
29
+ config.active_support.deprecation = :stderr
30
+ end
@@ -0,0 +1,7 @@
1
+ # Be sure to restart your server when you modify this file.
2
+
3
+ # Your secret key for verifying the integrity of signed cookies.
4
+ # If you change this key, all old signed cookies will become invalid!
5
+ # Make sure the secret is at least 30 characters and all random,
6
+ # no regular words or you'll be exposed to dictionary attacks.
7
+ Rails3::Application.config.secret_token = '74c293b4c5df1981d2f92785aa5538a21b0901293d693e6bb828669cbb1f1d1f5ddd1b3e21325304c90e952a4866a9eec7620379b6f6c27aae0670cdefda97ae'
@@ -0,0 +1,8 @@
1
+ # Be sure to restart your server when you modify this file.
2
+
3
+ Rails3::Application.config.session_store :cookie_store, :key => '_rails3_session'
4
+
5
+ # Use the database for sessions instead of the cookie-based default,
6
+ # which shouldn't be used to store highly confidential information
7
+ # (create the session table with "rails generate session_migration")
8
+ # Rails3::Application.config.session_store :active_record_store
@@ -0,0 +1,11 @@
1
+
2
+ development: &default
3
+ app_id: '123'
4
+ secret: '456'
5
+ canvas: 'can'
6
+
7
+ production:
8
+ *default
9
+
10
+ test:
11
+ *default
@@ -0,0 +1,5 @@
1
+
2
+ Rails3::Application.routes.draw do
3
+ root :controller => 'application', :action => 'index'
4
+ match ':action', :controller => 'application'
5
+ end
@@ -0,0 +1,183 @@
1
+
2
+ require 'test_helper'
3
+ require 'webmock'
4
+
5
+ WebMock.disable_net_connect!
6
+
7
+ class ApplicationControllerTest < ActionController::TestCase
8
+ include WebMock::API
9
+
10
+ def setup
11
+ body = rand(2) == 0 ? '{"error":{"type":"OAuthException"}}' :
12
+ '{"error_code":104}'
13
+
14
+ stub_request(:get, 'https://graph.facebook.com/me').
15
+ to_return(:body => body)
16
+ end
17
+
18
+ def teardown
19
+ WebMock.reset!
20
+ end
21
+
22
+ def assert_url expected
23
+ assert_equal(expected, normalize_url(assigns(:rest_graph_authorize_url)))
24
+ if @response.status == 200 # js redirect
25
+ assert_equal(
26
+ expected,
27
+ normalize_url(
28
+ @response.body.match(/window\.top\.location\.href = '(.+?)'/)[1]))
29
+
30
+ assert_equal(
31
+ CGI.escapeHTML(expected),
32
+ normalize_url(
33
+ @response.body.match(/content="0;url=(.+?)"/)[1], '&amp;'))
34
+
35
+ assert_equal(
36
+ CGI.escapeHTML(expected),
37
+ normalize_url(
38
+ @response.body.match(/<a href="(.+?)" target="_top">/)[1], '&amp;'))
39
+ end
40
+ end
41
+
42
+ def test_index
43
+ get(:index)
44
+ assert_response :redirect
45
+
46
+ url = normalize_url(
47
+ 'https://graph.facebook.com/oauth/authorize?client_id=123&' \
48
+ 'scope=&redirect_uri=http%3A%2F%2Ftest.host%2F')
49
+
50
+ assert_url(url)
51
+ end
52
+
53
+ def test_canvas
54
+ get(:canvas)
55
+ assert_response :success
56
+
57
+ url = normalize_url(
58
+ 'https://graph.facebook.com/oauth/authorize?client_id=123&' \
59
+ 'scope=publish_stream&' \
60
+ 'redirect_uri=http%3A%2F%2Fapps.facebook.com%2Fcan%2Fcanvas')
61
+
62
+ assert_url(url)
63
+ end
64
+
65
+ def test_diff_canvas
66
+ get(:diff_canvas)
67
+ assert_response :success
68
+
69
+ url = normalize_url(
70
+ 'https://graph.facebook.com/oauth/authorize?client_id=123&' \
71
+ 'scope=email&' \
72
+ 'redirect_uri=http%3A%2F%2Fapps.facebook.com%2FToT%2Fdiff_canvas')
73
+
74
+ assert_url(url)
75
+ end
76
+
77
+ def test_iframe_canvas
78
+ get(:iframe_canvas)
79
+ assert_response :success
80
+
81
+ url = normalize_url(
82
+ 'https://graph.facebook.com/oauth/authorize?client_id=123&' \
83
+ 'scope=&' \
84
+ 'redirect_uri=http%3A%2F%2Fapps.facebook.com%2Fzzz%2Fiframe_canvas')
85
+
86
+ assert_url(url)
87
+ end
88
+
89
+ def test_options
90
+ get(:options)
91
+ assert_response :redirect
92
+
93
+ url = normalize_url(
94
+ 'https://graph.facebook.com/oauth/authorize?client_id=123&' \
95
+ 'scope=bogus&' \
96
+ 'redirect_uri=http%3A%2F%2Ftest.host%2Foptions')
97
+
98
+ assert_url(url)
99
+ end
100
+
101
+ def test_protected
102
+ assert_nil @controller.public_methods.find{ |m| m.to_s =~ /^rest_graph/ }
103
+ end
104
+
105
+ def test_no_auto
106
+ get(:no_auto)
107
+ assert_response :success
108
+ assert_equal 'XD', @response.body
109
+ end
110
+
111
+ def test_app_id
112
+ get(:diff_app_id)
113
+ assert_response :success
114
+ assert_equal 'zzz', @response.body
115
+ end
116
+
117
+ def test_cache
118
+ WebMock.reset!
119
+ stub_request(:get, 'https://graph.facebook.com/cache').
120
+ to_return(:body => '{"message":"ok"}')
121
+
122
+ get(:cache)
123
+ assert_response :success
124
+ assert_equal '{"message":"ok"}', @response.body
125
+ end
126
+
127
+ def test_handler
128
+ WebMock.reset!
129
+ stub_request(:get, 'https://graph.facebook.com/me?access_token=aloha').
130
+ to_return(:body => '["snowman"]')
131
+
132
+ Rails.cache[:fbs] = RestGraph.new(:access_token => 'aloha').fbs
133
+ get(:handler_)
134
+ assert_response :success
135
+ assert_equal '["snowman"]', @response.body
136
+ ensure
137
+ Rails.cache.clear
138
+ end
139
+
140
+ def test_session
141
+ WebMock.reset!
142
+ stub_request(:get, 'https://graph.facebook.com/me?access_token=wozilla').
143
+ to_return(:body => '["fireball"]')
144
+
145
+ @request.session[RestGraph::RailsUtil.rest_graph_storage_key] =
146
+ RestGraph.new(:access_token => 'wozilla').fbs
147
+
148
+ get(:session_)
149
+ assert_response :success
150
+ assert_equal '["fireball"]', @response.body
151
+ end
152
+
153
+ def test_cookies
154
+ WebMock.reset!
155
+ stub_request(:get, 'https://graph.facebook.com/me?access_token=blizzard').
156
+ to_return(:body => '["yeti"]')
157
+
158
+ @request.cookies[RestGraph::RailsUtil.rest_graph_storage_key] =
159
+ RestGraph.new(:access_token => 'blizzard').fbs
160
+
161
+ get(:cookies_)
162
+ assert_response :success
163
+ assert_equal '["yeti"]', @response.body
164
+ end
165
+
166
+ def test_error
167
+ get(:error)
168
+ rescue => e
169
+ assert_equal RestGraph::Error, e.class
170
+ end
171
+
172
+ def test_reinitailize
173
+ get(:reinitialize)
174
+ assert_response :success
175
+ assert_equal [nil, {'a' => 'b'}], YAML.load(@response.body)
176
+ end
177
+
178
+ def test_helper
179
+ get(:helper)
180
+ assert_response :success
181
+ assert_equal RestGraph.default_app_id, @response.body.strip
182
+ end
183
+ end
@@ -0,0 +1,18 @@
1
+
2
+ ENV["RAILS_ENV"] = "test"
3
+ require File.expand_path('../../config/environment', __FILE__)
4
+ begin
5
+ require 'rails/test_help'
6
+ rescue LoadError # for rails2
7
+ require 'test_help'
8
+ end
9
+
10
+ class ActiveSupport::TestCase
11
+ def normalize_query query, amp='&'
12
+ '?' + query[1..-1].split(amp).sort.join(amp)
13
+ end
14
+
15
+ def normalize_url url, amp='&'
16
+ url.sub(/\?.+/){ |query| normalize_query(query, amp) }
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+
2
+ require 'test_helper'
3
+ require 'rr'
4
+
5
+ class RailsUtilTest < ActiveSupport::TestCase
6
+ include RR::Adapters::TestUnit
7
+
8
+ def setup_mock url
9
+ mock(RestGraph::RailsUtil).rest_graph_in_canvas?{ false }
10
+ mock(RestGraph::RailsUtil).request{
11
+ mock(Object.new).url{ url }
12
+ }
13
+ end
14
+
15
+ def test_rest_graph_normalized_request_uri_0
16
+ setup_mock( 'http://test.com/?code=123&lang=en')
17
+ assert_equal('http://test.com/?lang=en',
18
+ RestGraph::RailsUtil.rest_graph_normalized_request_uri)
19
+ end
20
+
21
+ def test_rest_graph_normalized_request_uri_1
22
+ setup_mock( 'http://test.com/?lang=en&code=123')
23
+ assert_equal('http://test.com/?lang=en',
24
+ RestGraph::RailsUtil.rest_graph_normalized_request_uri)
25
+ end
26
+
27
+ def test_rest_graph_normalized_request_uri_2
28
+ setup_mock( 'http://test.com/?session=abc&lang=en&code=123')
29
+ assert_equal('http://test.com/?lang=en',
30
+ RestGraph::RailsUtil.rest_graph_normalized_request_uri)
31
+ end
32
+
33
+ def test_rest_graph_normalized_request_uri_3
34
+ setup_mock( 'http://test.com/?code=123')
35
+ assert_equal('http://test.com/',
36
+ RestGraph::RailsUtil.rest_graph_normalized_request_uri)
37
+ end
38
+
39
+ def test_rest_graph_normalized_request_uri_4
40
+ setup_mock( 'http://test.com/?signed_request=abc&code=123')
41
+ assert_equal('http://test.com/',
42
+ RestGraph::RailsUtil.rest_graph_normalized_request_uri)
43
+ end
44
+ end
data/init.rb CHANGED
@@ -1,2 +1,2 @@
1
1
 
2
- require 'rest-graph/autoload'
2
+ require 'rest-graph'
data/lib/rest-graph.rb CHANGED
@@ -1,573 +1,7 @@
1
1
 
2
- # optional http client
3
- begin; require 'restclient' ; rescue LoadError; end
4
- begin; require 'em-http-request'; rescue LoadError; end
2
+ require 'rest-graph/core'
3
+ require 'rest-graph/config_util'
4
+ require 'rest-graph/facebook_util'
5
+ require 'rest-graph/version'
5
6
 
6
- # optional gem
7
- begin; require 'rack' ; rescue LoadError; end
8
-
9
- # stdlib
10
- require 'digest/md5'
11
- require 'openssl'
12
-
13
- require 'cgi'
14
- require 'timeout'
15
-
16
- # the data structure used in RestGraph
17
- RestGraphStruct = Struct.new(:auto_decode, :strict, :timeout,
18
- :graph_server, :old_server,
19
- :accept, :lang,
20
- :app_id, :secret,
21
- :data, :cache,
22
- :log_method,
23
- :log_handler,
24
- :error_handler) unless defined?(RestGraphStruct)
25
-
26
- class RestGraph < RestGraphStruct
27
- EventStruct = Struct.new(:duration, :url) unless
28
- defined?(::RestGraph::EventStruct)
29
-
30
- Attributes = RestGraphStruct.members.map(&:to_sym) unless
31
- defined?(::RestGraph::Attributes)
32
-
33
- class Event < EventStruct
34
- # self.class.name[/(?<=::)\w+$/] if RUBY_VERSION >= '1.9.2'
35
- def name; self.class.name[/::\w+$/].tr(':', ''); end
36
- def to_s; "RestGraph: spent #{sprintf('%f', duration)} #{name} #{url}";end
37
- end
38
- class Event::MultiDone < Event; end
39
- class Event::Requested < Event; end
40
- class Event::CacheHit < Event; end
41
- class Event::Failed < Event; end
42
-
43
- class Error < RuntimeError
44
- class AccessToken < Error; end
45
- class InvalidAccessToken < AccessToken; end
46
- class MissingAccessToken < AccessToken; end
47
-
48
- attr_reader :error, :url
49
- def initialize error, url=''
50
- @error, @url = error, url
51
- super("#{error.inspect} from #{url}")
52
- end
53
-
54
- module Util
55
- extend self
56
- def parse error, url=''
57
- return Error.new(error, url) unless error.kind_of?(Hash)
58
- if invalid_token?(error)
59
- InvalidAccessToken.new(error, url)
60
- elsif missing_token?(error)
61
- MissingAccessToken.new(error, url)
62
- else
63
- Error.new(error, url)
64
- end
65
- end
66
-
67
- def invalid_token? error
68
- (%w[OAuthInvalidTokenException
69
- OAuthException].include?((error['error'] || {})['type'])) ||
70
- (error['error_code'] == 190) # Invalid OAuth 2.0 Access Token
71
- end
72
-
73
- def missing_token? error
74
- (error['error'] || {})['message'] =~ /^An active access token/ ||
75
- (error['error_code'] == 104) # Requires valid signature
76
- end
77
- end
78
- extend Util
79
- end
80
-
81
- # honor default attributes
82
- Attributes.each{ |name|
83
- module_eval <<-RUBY
84
- def #{name}
85
- (r = super).nil? ? (self.#{name} = self.class.default_#{name}) : r
86
- end
87
- RUBY
88
- }
89
-
90
- # setup defaults
91
- module DefaultAttributes
92
- extend self
93
- def default_auto_decode ; true ; end
94
- def default_strict ; false ; end
95
- def default_timeout ; 10 ; end
96
- def default_graph_server; 'https://graph.facebook.com/'; end
97
- def default_old_server ; 'https://api.facebook.com/' ; end
98
- def default_accept ; 'text/javascript' ; end
99
- def default_lang ; 'en-us' ; end
100
- def default_app_id ; nil ; end
101
- def default_secret ; nil ; end
102
- def default_data ; {} ; end
103
- def default_cache ; nil ; end
104
- def default_log_method ; nil ; end
105
- def default_log_handler ; nil ; end
106
- def default_error_handler
107
- lambda{ |error, url| raise ::RestGraph::Error.parse(error, url) }
108
- end
109
- end
110
- extend DefaultAttributes
111
-
112
- # Fallback to ruby-hmac gem in case system openssl
113
- # lib doesn't support SHA256 (OSX 10.5)
114
- def self.hmac_sha256 key, data
115
- # for ruby version >= 1.8.7, we can simply pass sha256,
116
- # instead of OpenSSL::Digest::Digest.new('sha256')
117
- # i'll go back to original implementation once all old systems died
118
- OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha256'), key, data)
119
- rescue RuntimeError
120
- require 'hmac-sha2'
121
- HMAC::SHA256.digest(key, data)
122
- end
123
-
124
- # begin json backend adapter
125
- module YajlRuby
126
- def self.extended mod
127
- mod.const_set(:ParseError, Yajl::ParseError)
128
- end
129
- def json_encode hash
130
- Yajl::Encoder.encode(hash)
131
- end
132
- def json_decode json
133
- Yajl::Parser.parse(json)
134
- end
135
- end
136
-
137
- module Json
138
- def self.extended mod
139
- mod.const_set(:ParseError, JSON::ParserError)
140
- end
141
- def json_encode hash
142
- JSON.dump(hash)
143
- end
144
- def json_decode json
145
- JSON.parse(json)
146
- end
147
- end
148
-
149
- module Gsub
150
- class ParseError < RuntimeError; end
151
- def self.extended mod
152
- mod.const_set(:ParseError, Gsub::ParseError)
153
- end
154
- # only works for flat hash
155
- def json_encode hash
156
- middle = hash.inject([]){ |r, (k, v)|
157
- r << "\"#{k}\":\"#{v.gsub('"','\\"')}\""
158
- }.join(',')
159
- "{#{middle}}"
160
- end
161
- def json_decode json
162
- raise NotImplementedError.new(
163
- 'You need to install either yajl-ruby, json, or json_pure gem')
164
- end
165
- end
166
-
167
- def self.select_json! picked=false
168
- if defined?(::Yajl)
169
- extend YajlRuby
170
- elsif defined?(::JSON)
171
- extend Json
172
- elsif picked
173
- extend Gsub
174
- else
175
- # pick a json gem if available
176
- %w[yajl json].each{ |json|
177
- begin
178
- require json
179
- break
180
- rescue LoadError
181
- end
182
- }
183
- select_json!(true)
184
- end
185
- end
186
- select_json! unless respond_to?(:json_decode)
187
- # end json backend adapter
188
-
189
-
190
-
191
-
192
-
193
- # common methods
194
-
195
- def initialize o={}
196
- (Attributes + [:access_token]).each{ |name|
197
- send("#{name}=", o[name]) if o.key?(name)
198
- }
199
- end
200
-
201
- def access_token
202
- data['access_token'] || data['oauth_token']
203
- end
204
-
205
- def access_token= token
206
- data['access_token'] = token
207
- end
208
-
209
- def authorized?
210
- !!access_token
211
- end
212
-
213
- def secret_access_token
214
- "#{app_id}|#{secret}"
215
- end
216
-
217
- def lighten!
218
- [:cache, :log_method, :log_handler, :error_handler].each{ |obj|
219
- send("#{obj}=", nil) }
220
- self
221
- end
222
-
223
- def lighten
224
- dup.lighten!
225
- end
226
-
227
- def inspect
228
- super.gsub(/(\w+)=([^,>]+)/){ |match|
229
- value = $2 == 'nil' ? self.class.send("default_#{$1}").inspect : $2
230
- "#{$1}=#{value}"
231
- }
232
- end
233
-
234
-
235
-
236
-
237
-
238
- # graph api related methods
239
-
240
- def url path, query={}, server=graph_server, opts={}
241
- "#{server}#{path}#{build_query_string(query, opts)}"
242
- end
243
-
244
- def get path, query={}, opts={}, &cb
245
- request(opts, [:get , url(path, query, graph_server, opts)], &cb)
246
- end
247
-
248
- def delete path, query={}, opts={}, &cb
249
- request(opts, [:delete, url(path, query, graph_server, opts)], &cb)
250
- end
251
-
252
- def post path, payload={}, query={}, opts={}, &cb
253
- request(opts, [:post , url(path, query, graph_server, opts), payload],
254
- &cb)
255
- end
256
-
257
- def put path, payload={}, query={}, opts={}, &cb
258
- request(opts, [:put , url(path, query, graph_server, opts), payload],
259
- &cb)
260
- end
261
-
262
- # request by eventmachine (em-http)
263
-
264
- def aget path, query={}, opts={}, &cb
265
- get(path, query, {:async => true}.merge(opts), &cb)
266
- end
267
-
268
- def adelete path, query={}, opts={}, &cb
269
- delete(path, query, {:async => true}.merge(opts), &cb)
270
- end
271
-
272
- def apost path, payload={}, query={}, opts={}, &cb
273
- post(path, payload, query, {:async => true}.merge(opts), &cb)
274
- end
275
-
276
- def aput path, payload={}, query={}, opts={}, &cb
277
- put(path, payload, query, {:async => true}.merge(opts), &cb)
278
- end
279
-
280
- def multi reqs, opts={}, &cb
281
- request({:async => true}.merge(opts),
282
- *reqs.map{ |(meth, path, query, payload)|
283
- [meth, url(path, query || {}, graph_server, opts), payload]
284
- }, &cb)
285
- end
286
-
287
-
288
-
289
-
290
-
291
- def next_page hash, opts={}, &cb
292
- if hash['paging'].kind_of?(Hash) && hash['paging']['next']
293
- request(opts, [:get, hash['paging']['next']], &cb)
294
- else
295
- yield(nil) if block_given?
296
- end
297
- end
298
-
299
- def prev_page hash, opts={}, &cb
300
- if hash['paging'].kind_of?(Hash) && hash['paging']['previous']
301
- request(opts, [:get, hash['paging']['previous']], &cb)
302
- else
303
- yield(nil) if block_given?
304
- end
305
- end
306
- alias_method :previous_page, :prev_page
307
-
308
- def for_pages hash, pages=1, opts={}, kind=:next_page, &cb
309
- if pages > 1
310
- merge_data(send(kind, hash, opts){ |result|
311
- yield(result.freeze) if block_given?
312
- for_pages(result, pages - 1, opts, kind, &cb) if result
313
- }, hash)
314
- else
315
- yield(nil) if block_given?
316
- hash
317
- end
318
- end
319
-
320
-
321
-
322
-
323
-
324
- # cookies, app_id, secrect related below
325
-
326
- def parse_rack_env! env
327
- env['HTTP_COOKIE'].to_s =~ /fbs_#{app_id}=([^\;]+)/
328
- self.data = parse_fbs!($1)
329
- end
330
-
331
- def parse_cookies! cookies
332
- self.data = parse_fbs!(cookies["fbs_#{app_id}"])
333
- end
334
-
335
- def parse_fbs! fbs
336
- self.data = check_sig_and_return_data(
337
- # take out facebook sometimes there but sometimes not quotes in cookies
338
- Rack::Utils.parse_query(fbs.to_s.gsub('"', '')))
339
- end
340
-
341
- def parse_json! json
342
- self.data = json &&
343
- check_sig_and_return_data(self.class.json_decode(json))
344
- rescue ParseError
345
- end
346
-
347
- def fbs
348
- "#{fbs_without_sig(data).join('&')}&sig=#{calculate_sig(data)}"
349
- end
350
-
351
- # facebook's new signed_request...
352
-
353
- def parse_signed_request! request
354
- sig_encoded, json_encoded = request.split('.')
355
- sig, json = [sig_encoded, json_encoded].map{ |str|
356
- "#{str.tr('-_', '+/')}==".unpack('m').first
357
- }
358
- self.data = self.class.json_decode(json) if
359
- secret && self.class.hmac_sha256(secret, json_encoded) == sig
360
- rescue ParseError
361
- end
362
-
363
-
364
-
365
-
366
-
367
- # oauth related
368
-
369
- def authorize_url opts={}
370
- query = {:client_id => app_id, :access_token => nil}.merge(opts)
371
- "#{graph_server}oauth/authorize#{build_query_string(query)}"
372
- end
373
-
374
- def authorize! opts={}
375
- query = {:client_id => app_id, :client_secret => secret}.merge(opts)
376
- self.data = Rack::Utils.parse_query(
377
- request({:auto_decode => false}.merge(opts),
378
- [:get, url('oauth/access_token', query)]))
379
- end
380
-
381
-
382
-
383
-
384
-
385
- # old rest facebook api, i will definitely love to remove them someday
386
-
387
- def old_rest path, query={}, opts={}, &cb
388
- request(
389
- opts,
390
- [:get,
391
- url("method/#{path}", {:format => 'json'}.merge(query),
392
- old_server, opts)],
393
- &cb)
394
- end
395
-
396
- def secret_old_rest path, query={}, opts={}, &cb
397
- old_rest(path, query, {:secret => true}.merge(opts), &cb)
398
- end
399
- alias_method :broken_old_rest, :secret_old_rest
400
-
401
- def exchange_sessions query={}, opts={}, &cb
402
- q = {:client_id => app_id, :client_secret => secret,
403
- :type => 'client_cred'}.merge(query)
404
- request(opts, [:post, url('oauth/exchange_sessions', q)], &cb)
405
- end
406
-
407
- def fql code, query={}, opts={}, &cb
408
- old_rest('fql.query', {:query => code}.merge(query), opts, &cb)
409
- end
410
-
411
- def fql_multi codes, query={}, opts={}, &cb
412
- old_rest('fql.multiquery',
413
- {:queries => self.class.json_encode(codes)}.merge(query), opts, &cb)
414
- end
415
-
416
-
417
-
418
-
419
-
420
- private
421
- def request opts, *reqs, &cb
422
- Timeout.timeout(timeout){
423
- if opts[:async]
424
- request_em(opts, reqs, &cb)
425
- else
426
- request_rc(opts, *reqs.first, &cb)
427
- end
428
- }
429
- end
430
-
431
- def request_em opts, reqs
432
- start_time = Time.now
433
- rs = reqs.map{ |(meth, uri, payload)|
434
- r = EM::HttpRequest.new(uri).send(meth, :body => payload)
435
- if cached = cache_get(uri)
436
- # TODO: this is hack!!
437
- r.instance_variable_set('@response', cached)
438
- r.instance_variable_set('@state' , :finish)
439
- r.on_request_complete
440
- r.succeed(r)
441
- else
442
- r.callback{
443
- cache_for(uri, r.response, meth)
444
- log(Event::Requested.new(Time.now - start_time, uri))
445
- }
446
- r.error{
447
- log(Event::Failed.new(Time.now - start_time, uri))
448
- }
449
- end
450
- r
451
- }
452
- EM::MultiRequest.new(rs){ |m|
453
- # TODO: how to deal with the failed?
454
- clients = m.responses[:succeeded]
455
- results = clients.map{ |client|
456
- post_request(client.response, client.uri)
457
- }
458
-
459
- if reqs.size == 1
460
- yield(results.first)
461
- else
462
- log(Event::MultiDone.new(Time.now - start_time,
463
- clients.map(&:uri).join(', ')))
464
- yield(results)
465
- end
466
- }
467
- end
468
-
469
- def request_rc opts, meth, uri, payload=nil, &cb
470
- start_time = Time.now
471
- post_request(cache_get(uri) || fetch(meth, uri, payload), uri, opts, &cb)
472
- rescue RestClient::Exception => e
473
- post_request(e.http_body, uri, opts, &cb)
474
- ensure
475
- log(Event::Requested.new(Time.now - start_time, uri))
476
- end
477
-
478
- def build_query_string query={}, opts={}
479
- token = opts[:secret] ? secret_access_token : access_token
480
- qq = token ? {:access_token => token}.merge(query) : query
481
- q = qq.select{ |k, v| v } # compacting the hash
482
- return '' if q.empty?
483
- return '?' + q.map{ |(k, v)| "#{k}=#{CGI.escape(v.to_s)}" }.join('&')
484
- end
485
-
486
- def build_headers
487
- headers = {}
488
- headers['Accept'] = accept if accept
489
- headers['Accept-Language'] = lang if lang
490
- headers
491
- end
492
-
493
- def post_request result, uri='', opts={}, &cb
494
- if decode?(opts)
495
- # [this].first is not needed for yajl-ruby
496
- decoded = self.class.json_decode("[#{result}]").first
497
- check_error(decoded, uri, &cb)
498
- else
499
- block_given? ? yield(result) : result
500
- end
501
- rescue ParseError => error
502
- error_handler.call(error, uri) if error_handler
503
- end
504
-
505
- def decode? opts
506
- if opts.has_key?(:auto_decode)
507
- opts[:auto_decode]
508
- elsif opts.has_key?(:suppress_decode)
509
- !opts[:suppress_decode]
510
- else
511
- auto_decode
512
- end
513
- end
514
-
515
- def check_sig_and_return_data cookies
516
- cookies if secret && calculate_sig(cookies) == cookies['sig']
517
- end
518
-
519
- def check_error hash, uri
520
- if error_handler && hash.kind_of?(Hash) &&
521
- (hash['error'] || # from graph api
522
- hash['error_code']) # from fql
523
- error_handler.call(hash, uri)
524
- else
525
- block_given? ? yield(hash) : hash
526
- end
527
- end
528
-
529
- def calculate_sig cookies
530
- Digest::MD5.hexdigest(fbs_without_sig(cookies).join + secret)
531
- end
532
-
533
- def fbs_without_sig cookies
534
- cookies.reject{ |(k, v)| k == 'sig' }.sort.map{ |a| a.join('=') }
535
- end
536
-
537
- def cache_key uri
538
- Digest::MD5.hexdigest(uri)
539
- end
540
-
541
- def cache_get uri
542
- return unless cache
543
- start_time = Time.now
544
- cache[cache_key(uri)].tap{ |result|
545
- log(Event::CacheHit.new(Time.now - start_time, uri)) if result
546
- }
547
- end
548
-
549
- def cache_for uri, result, meth
550
- cache[cache_key(uri)] = result if cache && meth == :get
551
- end
552
-
553
- def fetch meth, uri, payload
554
- RestClient::Request.execute(:method => meth, :url => uri,
555
- :headers => build_headers,
556
- :payload => payload).body.
557
- tap{ |result| cache_for(uri, result, meth) }
558
- end
559
-
560
- def merge_data lhs, rhs
561
- [lhs, rhs].each{ |hash|
562
- return rhs.reject{ |k, v| k == 'paging' } if
563
- !hash.kind_of?(Hash) || !hash['data'].kind_of?(Array)
564
- }
565
- lhs['data'].unshift(*rhs['data'])
566
- lhs
567
- end
568
-
569
- def log event
570
- log_handler.call(event) if log_handler
571
- log_method .call("DEBUG: #{event}") if log_method
572
- end
573
- end
7
+ require 'rest-graph/rails_util' if Object.const_defined?(:Rails)