rest-graph 1.1.1 → 1.2.0
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/CHANGES +33 -0
- data/README.rdoc +50 -11
- data/TODO +3 -15
- data/example/rails/README +4 -0
- data/example/rails/Rakefile +10 -0
- data/example/rails/app/controllers/application_controller.rb +20 -0
- data/example/rails/app/views/rest-graph.erb +16 -0
- data/example/rails/config/boot.rb +110 -0
- data/example/rails/config/environment.rb +43 -0
- data/example/rails/config/environments/development.rb +17 -0
- data/example/rails/config/environments/production.rb +28 -0
- data/example/rails/config/environments/test.rb +28 -0
- data/example/rails/config/initializers/cookie_verification_secret.rb +7 -0
- data/example/rails/config/initializers/new_rails_defaults.rb +21 -0
- data/example/rails/config/initializers/session_store.rb +15 -0
- data/example/rails/config/rest-graph.yaml +5 -0
- data/example/rails/config/routes.rb +43 -0
- data/example/rails/script/console +3 -0
- data/example/rails/script/server +3 -0
- data/init.rb +1 -0
- data/lib/rest-graph.rb +75 -35
- data/lib/rest-graph/load_config.rb +33 -34
- data/lib/rest-graph/rails_controller.rb +100 -0
- data/lib/rest-graph/version.rb +1 -3
- data/rest-graph.gemspec +22 -20
- data/test/common.rb +7 -1
- data/test/test_default.rb +31 -0
- data/test/test_fql.rb +60 -0
- data/test/test_handler.rb +56 -0
- data/test/test_load_config.rb +4 -0
- data/test/test_parse.rb +70 -0
- data/test/test_rest-graph.rb +4 -80
- metadata +40 -10
@@ -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
|
+
ActionController::Base.cookie_verifier_secret = '095e8e5c0b6b901901efb23fab50005b68d9d6a9d41f4ec780946cff34b26603762bc0ea1baf204613b252e5ae499d38b232d5b75edac513a723e450e76548a3';
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Be sure to restart your server when you modify this file.
|
2
|
+
|
3
|
+
# These settings change the behavior of Rails 2 apps and will be defaults
|
4
|
+
# for Rails 3. You can remove this initializer when Rails 3 is released.
|
5
|
+
|
6
|
+
if defined?(ActiveRecord)
|
7
|
+
# Include Active Record class name as root for JSON serialized output.
|
8
|
+
ActiveRecord::Base.include_root_in_json = true
|
9
|
+
|
10
|
+
# Store the full class name (including module namespace) in STI type column.
|
11
|
+
ActiveRecord::Base.store_full_sti_class = true
|
12
|
+
end
|
13
|
+
|
14
|
+
ActionController::Routing.generate_best_match = false
|
15
|
+
|
16
|
+
# Use ISO 8601 format for JSON serialized times and dates.
|
17
|
+
ActiveSupport.use_standard_json_time_format = true
|
18
|
+
|
19
|
+
# Don't escape HTML entities in JSON, leave that for the #json_escape helper.
|
20
|
+
# if you're including raw json in an HTML page.
|
21
|
+
ActiveSupport.escape_html_entities_in_json = false
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Be sure to restart your server when you modify this file.
|
2
|
+
|
3
|
+
# Your secret key for verifying cookie session data integrity.
|
4
|
+
# If you change this key, all old sessions 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
|
+
ActionController::Base.session = {
|
8
|
+
:key => '_rails_session',
|
9
|
+
:secret => 'c99a19ce0dc4ed1809e32b6b43bd9229c3a504c456230119dd445fdcb63c0ce06b436f1bf1eace27ebbe0da6041ff2b65cbb4ae4beadc3077e3e6ae07ea75118'
|
10
|
+
}
|
11
|
+
|
12
|
+
# Use the database for sessions instead of the cookie-based default,
|
13
|
+
# which shouldn't be used to store highly confidential information
|
14
|
+
# (create the session table with "rake db:sessions:create")
|
15
|
+
# ActionController::Base.session_store = :active_record_store
|
@@ -0,0 +1,43 @@
|
|
1
|
+
ActionController::Routing::Routes.draw do |map|
|
2
|
+
# The priority is based upon order of creation: first created -> highest priority.
|
3
|
+
|
4
|
+
# Sample of regular route:
|
5
|
+
# map.connect 'products/:id', :controller => 'catalog', :action => 'view'
|
6
|
+
# Keep in mind you can assign values other than :controller and :action
|
7
|
+
|
8
|
+
# Sample of named route:
|
9
|
+
# map.purchase 'products/:id/purchase', :controller => 'catalog', :action => 'purchase'
|
10
|
+
# This route can be invoked with purchase_url(:id => product.id)
|
11
|
+
|
12
|
+
# Sample resource route (maps HTTP verbs to controller actions automatically):
|
13
|
+
# map.resources :products
|
14
|
+
|
15
|
+
# Sample resource route with options:
|
16
|
+
# map.resources :products, :member => { :short => :get, :toggle => :post }, :collection => { :sold => :get }
|
17
|
+
|
18
|
+
# Sample resource route with sub-resources:
|
19
|
+
# map.resources :products, :has_many => [ :comments, :sales ], :has_one => :seller
|
20
|
+
|
21
|
+
# Sample resource route with more complex sub-resources
|
22
|
+
# map.resources :products do |products|
|
23
|
+
# products.resources :comments
|
24
|
+
# products.resources :sales, :collection => { :recent => :get }
|
25
|
+
# end
|
26
|
+
|
27
|
+
# Sample resource route within a namespace:
|
28
|
+
# map.namespace :admin do |admin|
|
29
|
+
# # Directs /admin/products/* to Admin::ProductsController (app/controllers/admin/products_controller.rb)
|
30
|
+
# admin.resources :products
|
31
|
+
# end
|
32
|
+
|
33
|
+
# You can have the root of your site routed with map.root -- just remember to delete public/index.html.
|
34
|
+
map.root :controller => 'application'
|
35
|
+
|
36
|
+
# See how all your routes lay out with "rake routes"
|
37
|
+
|
38
|
+
# Install the default routes as the lowest priority.
|
39
|
+
# Note: These default routes make all actions in every controller accessible via GET requests. You should
|
40
|
+
# consider removing or commenting them out if you're using named routes and resources.
|
41
|
+
# map.connect ':controller/:action/:id'
|
42
|
+
# map.connect ':controller/:action/:id.:format'
|
43
|
+
end
|
data/init.rb
CHANGED
data/lib/rest-graph.rb
CHANGED
@@ -1,15 +1,35 @@
|
|
1
1
|
|
2
|
+
# gem
|
2
3
|
require 'rest_client'
|
3
4
|
|
5
|
+
# stdlib
|
6
|
+
require 'digest/md5'
|
4
7
|
require 'cgi'
|
5
8
|
|
9
|
+
# optional gem
|
10
|
+
begin
|
11
|
+
require 'rack'
|
12
|
+
rescue LoadError; end
|
13
|
+
|
14
|
+
begin
|
15
|
+
require 'json'
|
16
|
+
rescue LoadError
|
17
|
+
begin
|
18
|
+
require 'json_pure'
|
19
|
+
rescue LoadError; end
|
20
|
+
end
|
21
|
+
|
6
22
|
# the data structure used in RestGraph
|
7
23
|
RestGraphStruct = Struct.new(:data, :auto_decode,
|
8
24
|
:graph_server, :fql_server,
|
9
25
|
:accept, :lang,
|
10
|
-
:app_id, :secret
|
26
|
+
:app_id, :secret,
|
27
|
+
:error_handler,
|
28
|
+
:log_handler) unless defined?(RestGraphStruct)
|
11
29
|
|
12
30
|
class RestGraph < RestGraphStruct
|
31
|
+
class Error < RuntimeError; end
|
32
|
+
|
13
33
|
Attributes = RestGraphStruct.members.map(&:to_sym)
|
14
34
|
|
15
35
|
# honor default attributes
|
@@ -32,14 +52,19 @@ class RestGraph < RestGraphStruct
|
|
32
52
|
def default_lang ; 'en-us' ; end
|
33
53
|
def default_app_id ; nil ; end
|
34
54
|
def default_secret ; nil ; end
|
55
|
+
def default_error_handler
|
56
|
+
lambda{ |error| raise ::RestGraph::Error.new(error) }
|
57
|
+
end
|
58
|
+
def default_log_handler
|
59
|
+
lambda{ |duration, url| }
|
60
|
+
end
|
35
61
|
end
|
36
62
|
extend DefaultAttributes
|
37
63
|
|
38
|
-
def initialize o
|
64
|
+
def initialize o={}
|
39
65
|
(Attributes + [:access_token]).each{ |name|
|
40
66
|
send("#{name}=", o[name]) if o.key?(name)
|
41
67
|
}
|
42
|
-
check_arguments!
|
43
68
|
end
|
44
69
|
|
45
70
|
def access_token
|
@@ -54,27 +79,40 @@ class RestGraph < RestGraphStruct
|
|
54
79
|
!!access_token
|
55
80
|
end
|
56
81
|
|
57
|
-
def get path, opts
|
82
|
+
def get path, opts={}
|
58
83
|
request(graph_server, path, opts, :get)
|
59
84
|
end
|
60
85
|
|
61
|
-
def delete path, opts
|
86
|
+
def delete path, opts={}
|
62
87
|
request(graph_server, path, opts, :delete)
|
63
88
|
end
|
64
89
|
|
65
|
-
def post path, payload, opts
|
90
|
+
def post path, payload, opts={}
|
66
91
|
request(graph_server, path, opts, :post, payload)
|
67
92
|
end
|
68
93
|
|
69
|
-
def put path, payload, opts
|
94
|
+
def put path, payload, opts={}
|
70
95
|
request(graph_server, path, opts, :put, payload)
|
71
96
|
end
|
72
97
|
|
73
|
-
def fql query, opts
|
98
|
+
def fql query, opts={}
|
74
99
|
request(fql_server, 'method/fql.query',
|
75
100
|
{:query => query, :format => 'json'}.merge(opts), :get)
|
76
101
|
end
|
77
102
|
|
103
|
+
def fql_multi queries, opts={}
|
104
|
+
q = if queries.respond_to?(:to_json)
|
105
|
+
queries.to_json
|
106
|
+
else
|
107
|
+
middle = queries.inject([]){ |r, (k, v)|
|
108
|
+
r << "\"#{k}\":\"#{v.gsub('"','\\"')}\""
|
109
|
+
}.join(',')
|
110
|
+
"{#{middle}}"
|
111
|
+
end
|
112
|
+
request(fql_server, 'method/fql.multiquery',
|
113
|
+
{:queries => q, :format => 'json'}.merge(opts), :get)
|
114
|
+
end
|
115
|
+
|
78
116
|
# cookies, app_id, secrect related below
|
79
117
|
|
80
118
|
if RUBY_VERSION >= '1.9.1'
|
@@ -98,6 +136,12 @@ class RestGraph < RestGraphStruct
|
|
98
136
|
check_sig_and_return_data(Rack::Utils.parse_query(fbs[1..-2]))
|
99
137
|
end
|
100
138
|
|
139
|
+
def parse_json! json
|
140
|
+
self.data = json &&
|
141
|
+
check_sig_and_return_data(JSON.load(json))
|
142
|
+
rescue JSON::ParserError
|
143
|
+
end
|
144
|
+
|
101
145
|
# oauth related
|
102
146
|
|
103
147
|
def authorize_url opts={}
|
@@ -112,37 +156,21 @@ class RestGraph < RestGraphStruct
|
|
112
156
|
end
|
113
157
|
|
114
158
|
private
|
115
|
-
def check_arguments!
|
116
|
-
if auto_decode
|
117
|
-
begin
|
118
|
-
require 'json'
|
119
|
-
rescue LoadError
|
120
|
-
require 'json_pure'
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
if app_id && secret # want to parse access_token in cookies
|
125
|
-
require 'digest/md5'
|
126
|
-
require 'rack'
|
127
|
-
elsif app_id || secret
|
128
|
-
raise ArgumentError.new("You may want to pass both" \
|
129
|
-
" app_id(#{app_id.inspect}) and" \
|
130
|
-
" secret(#{secret.inspect})")
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
159
|
def request server, path, opts, method, payload=nil, suppress_decode=false
|
160
|
+
start_time = Time.now
|
161
|
+
res = RestClient::Resource.new(server)[path + build_query_string(opts)]
|
135
162
|
post_request(
|
136
|
-
|
137
|
-
send(method, *[payload, build_headers].compact), suppress_decode)
|
163
|
+
res.send(method, *[payload, build_headers].compact), suppress_decode)
|
138
164
|
rescue RestClient::InternalServerError => e
|
139
165
|
post_request(e.http_body, suppress_decode)
|
166
|
+
ensure
|
167
|
+
log_handler.call(Time.now - start_time, res.url)
|
140
168
|
end
|
141
169
|
|
142
|
-
def build_query_string
|
143
|
-
|
144
|
-
return '' if
|
145
|
-
return '?' +
|
170
|
+
def build_query_string query={}
|
171
|
+
q = query.merge(access_token ? {:access_token => access_token} : {})
|
172
|
+
return '' if q.empty?
|
173
|
+
return '?' + q.map{ |(k, v)| "#{k}=#{CGI.escape(v.to_s)}" }.join('&')
|
146
174
|
end
|
147
175
|
|
148
176
|
def build_headers
|
@@ -153,11 +181,23 @@ class RestGraph < RestGraphStruct
|
|
153
181
|
end
|
154
182
|
|
155
183
|
def post_request result, suppress_decode=false
|
156
|
-
|
184
|
+
if auto_decode && !suppress_decode
|
185
|
+
check_error(JSON.parse(result))
|
186
|
+
else
|
187
|
+
result
|
188
|
+
end
|
157
189
|
end
|
158
190
|
|
159
191
|
def check_sig_and_return_data cookies
|
160
|
-
cookies if calculate_sig(cookies) == cookies['sig']
|
192
|
+
cookies if secret && calculate_sig(cookies) == cookies['sig']
|
193
|
+
end
|
194
|
+
|
195
|
+
def check_error hash
|
196
|
+
if error_handler && hash.kind_of?(Hash) && hash['error']
|
197
|
+
error_handler.call(hash)
|
198
|
+
else
|
199
|
+
hash
|
200
|
+
end
|
161
201
|
end
|
162
202
|
|
163
203
|
def calculate_sig cookies
|
@@ -3,40 +3,39 @@ require 'erb'
|
|
3
3
|
require 'yaml'
|
4
4
|
|
5
5
|
require 'rest-graph'
|
6
|
+
require 'rest-graph/rails_controller' if Object.const_defined?(:Rails)
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
RestGraph.send(:extend, mod)
|
40
|
-
end
|
8
|
+
module RestGraph::LoadConfig
|
9
|
+
module_function
|
10
|
+
def auto_load!
|
11
|
+
RestGraph::LoadConfig.load_if_rails!
|
12
|
+
end
|
13
|
+
|
14
|
+
def load_if_rails!
|
15
|
+
return unless Object.const_defined?(:Rails)
|
16
|
+
root = Rails.root
|
17
|
+
file = ["#{root}/config/rest-graph.yaml", # YAML should use .yaml
|
18
|
+
"#{root}/config/rest-graph.yml"].find{|path| File.exist?(path)}
|
19
|
+
return unless file
|
20
|
+
|
21
|
+
RestGraph::LoadConfig.load_config!(file, Rails.env)
|
22
|
+
end
|
23
|
+
|
24
|
+
def load_config! file, env
|
25
|
+
config = YAML.load(ERB.new(File.read(file)).result(binding))
|
26
|
+
defaults = config[env]
|
27
|
+
return unless defaults
|
28
|
+
|
29
|
+
mod = Module.new
|
30
|
+
mod.module_eval(defaults.inject([]){ |r, (k, v)|
|
31
|
+
r << <<-RUBY
|
32
|
+
def default_#{k}
|
33
|
+
# quote strings, leave others free (e.g. false, numbers, etc)
|
34
|
+
#{v.kind_of?(String) ? "'#{v}'" : v}
|
35
|
+
end
|
36
|
+
RUBY
|
37
|
+
}.join)
|
38
|
+
|
39
|
+
RestGraph.send(:extend, mod)
|
41
40
|
end
|
42
41
|
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
|
2
|
+
require 'rest-graph'
|
3
|
+
|
4
|
+
module RestGraph::RailsController
|
5
|
+
module_function
|
6
|
+
# filters for you
|
7
|
+
def setup_iframe
|
8
|
+
@fb_sig_in_iframe = true
|
9
|
+
end
|
10
|
+
|
11
|
+
def setup_rest_graph
|
12
|
+
rest_graph_create
|
13
|
+
|
14
|
+
# exchange the code with access_token
|
15
|
+
if params[:code]
|
16
|
+
@rg.authorize!(:code => params[:code],
|
17
|
+
:redirect_uri => normalized_request_uri)
|
18
|
+
logger.debug(
|
19
|
+
"DEBUG: RestGraph: detected code with #{normalized_request_uri}, " \
|
20
|
+
"parsed: #{@rg.data.inspect}")
|
21
|
+
end
|
22
|
+
|
23
|
+
# if the code is bad or not existed,
|
24
|
+
# check if there's one in session,
|
25
|
+
# meanwhile, there the sig and access_token is correct,
|
26
|
+
# that means we're in the context of iframe
|
27
|
+
if !@rg.authorized? && params[:session]
|
28
|
+
@rg.parse_json!(params[:session])
|
29
|
+
logger.debug(
|
30
|
+
"DEBUG: RestGraph: detected session, parsed: #{@rg.data.inspect}")
|
31
|
+
|
32
|
+
if @rg.authorized?
|
33
|
+
@fb_sig_in_iframe = true
|
34
|
+
else
|
35
|
+
logger.warn("WARN: RestGraph: bad session: #{params[:session]}")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# if we're not in iframe nor code passed,
|
40
|
+
# we could check out cookies as well.
|
41
|
+
if !@rg.authorized?
|
42
|
+
@rg.parse_cookies!(cookies)
|
43
|
+
logger.debug(
|
44
|
+
"DEBUG: RestGraph: detected cookies, parsed: #{@rg.data.inspect}")
|
45
|
+
end
|
46
|
+
|
47
|
+
# there are above 3 ways to check the user identity!
|
48
|
+
# if nor of them passed, then we can suppose the user
|
49
|
+
# didn't authorize for us
|
50
|
+
end
|
51
|
+
|
52
|
+
# override this if you need different app_id and secret
|
53
|
+
def rest_graph_create
|
54
|
+
@rg ||= RestGraph.new(:error_handler => method(:rest_graph_authorize),
|
55
|
+
:log_handler => method(:rest_graph_log))
|
56
|
+
end
|
57
|
+
|
58
|
+
def rest_graph_authorize error=nil
|
59
|
+
logger.warn("WARN: RestGraph: #{error.inspect}") if error
|
60
|
+
|
61
|
+
@authorize_url = @rg.authorize_url(
|
62
|
+
{:redirect_uri => normalized_request_uri,
|
63
|
+
:scope => rest_graph_authorize_scope}.
|
64
|
+
merge(rest_graph_authorize_options))
|
65
|
+
|
66
|
+
logger.debug("DEBUG: RestGraph: redirect to #{@authorize_url}")
|
67
|
+
|
68
|
+
rest_graph_authorize_redirect
|
69
|
+
return false
|
70
|
+
end
|
71
|
+
|
72
|
+
# override this if you need different access scope
|
73
|
+
def rest_graph_authorize_scope
|
74
|
+
@rest_graph_authorize_scope ||=
|
75
|
+
'offline_access,publish_stream,read_friendlists'
|
76
|
+
end
|
77
|
+
|
78
|
+
# override this if you want the simple redirect_to
|
79
|
+
def rest_graph_authorize_redirect
|
80
|
+
render :template => 'rest-graph/authorization'
|
81
|
+
end
|
82
|
+
|
83
|
+
def rest_graph_authorize_options
|
84
|
+
@rest_graph_authorize_options ||= {}
|
85
|
+
end
|
86
|
+
|
87
|
+
def rest_graph_log duration, url
|
88
|
+
logger.debug("DEBUG: RestGraph: spent #{duration} requesting #{url}")
|
89
|
+
end
|
90
|
+
|
91
|
+
def normalized_request_uri
|
92
|
+
if @fb_sig_in_iframe
|
93
|
+
"http://apps.facebook.com/" \
|
94
|
+
"#{RestGraph.default_canvas}#{request.request_uri}"
|
95
|
+
else
|
96
|
+
request.url
|
97
|
+
end.sub(/[\&\?]session=[^\&]+/, '').
|
98
|
+
sub(/[\&\?]code=[^\&]+/, '')
|
99
|
+
end
|
100
|
+
end
|