Empact-rpx_now 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/CHANGELOG +38 -0
- data/Empact-rpx_now.gemspec +67 -0
- data/MIGRATION +9 -0
- data/README.markdown +156 -0
- data/Rakefile +20 -0
- data/VERSION +1 -0
- data/certs/ssl_cert.pem +3154 -0
- data/init.rb +2 -0
- data/lib/rpx_now.rb +163 -0
- data/lib/rpx_now/api.rb +81 -0
- data/lib/rpx_now/contacts_collection.rb +20 -0
- data/lib/rpx_now/user_integration.rb +9 -0
- data/lib/rpx_now/user_proxy.rb +19 -0
- data/spec/fixtures/get_contacts_response.json +58 -0
- data/spec/integration/mapping_spec.rb +40 -0
- data/spec/rpx_now/api_spec.rb +89 -0
- data/spec/rpx_now/contacts_collection_spec.rb +32 -0
- data/spec/rpx_now/user_proxy_spec.rb +33 -0
- data/spec/rpx_now_spec.rb +361 -0
- data/spec/spec_helper.rb +18 -0
- metadata +98 -0
data/init.rb
ADDED
data/lib/rpx_now.rb
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'rpx_now/api'
|
2
|
+
require 'rpx_now/contacts_collection'
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
module RPXNow
|
6
|
+
extend self
|
7
|
+
|
8
|
+
attr_accessor :api_key
|
9
|
+
attr_accessor :api_version
|
10
|
+
attr_accessor :ssl
|
11
|
+
self.api_version = 2
|
12
|
+
self.ssl = true
|
13
|
+
|
14
|
+
VERSION = File.read( File.join(File.dirname(__FILE__),'..','VERSION') ).strip
|
15
|
+
|
16
|
+
# retrieve the users data
|
17
|
+
# - cleaned Hash
|
18
|
+
# - complete/unclean response when block was given user_data{|response| ...; return hash }
|
19
|
+
# - nil when token was invalid / data was not found
|
20
|
+
def user_data(token, options={})
|
21
|
+
begin
|
22
|
+
data = Api.call("auth_info", options.merge(:token => token))
|
23
|
+
result = (block_given? ? yield(data) : parse_user_data(data, options))
|
24
|
+
result.respond_to?(:with_indifferent_access) ? result.with_indifferent_access : result
|
25
|
+
rescue ServerError
|
26
|
+
return nil if $!.to_s=~/Data not found/
|
27
|
+
raise
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# set the users status
|
32
|
+
def set_status(identifier, status, options={})
|
33
|
+
options = options.merge(:identifier => identifier, :status => status)
|
34
|
+
data = Api.call("set_status", options)
|
35
|
+
rescue ServerError
|
36
|
+
return nil if $!.to_s=~/Data not found/
|
37
|
+
raise
|
38
|
+
end
|
39
|
+
|
40
|
+
# Post an activity update to the user's activity stream.
|
41
|
+
# See more: https://rpxnow.com/docs#api_activity
|
42
|
+
def activity(identifier, activity_options, options={})
|
43
|
+
options = options.merge(:identifier => identifier, :activity => activity_options.to_json)
|
44
|
+
Api.call("activity", options)
|
45
|
+
end
|
46
|
+
|
47
|
+
# maps an identifier to an primary-key (e.g. user.id)
|
48
|
+
def map(identifier, primary_key, options={})
|
49
|
+
Api.call("map", options.merge(:identifier => identifier, :primaryKey => primary_key))
|
50
|
+
end
|
51
|
+
|
52
|
+
# un-maps an identifier to an primary-key (e.g. user.id)
|
53
|
+
def unmap(identifier, primary_key, options={})
|
54
|
+
Api.call("unmap", options.merge(:identifier => identifier, :primaryKey => primary_key))
|
55
|
+
end
|
56
|
+
|
57
|
+
# returns an array of identifiers which are mapped to one of your primary-keys (e.g. user.id)
|
58
|
+
def mappings(primary_key, options={})
|
59
|
+
Api.call("mappings", options.merge(:primaryKey => primary_key))['identifiers']
|
60
|
+
end
|
61
|
+
|
62
|
+
def all_mappings(options={})
|
63
|
+
Api.call("all_mappings", options)['mappings']
|
64
|
+
end
|
65
|
+
|
66
|
+
def contacts(identifier, options={})
|
67
|
+
data = Api.call("get_contacts", options.merge(:identifier => identifier))
|
68
|
+
RPXNow::ContactsCollection.new(data['response'])
|
69
|
+
end
|
70
|
+
alias get_contacts contacts
|
71
|
+
|
72
|
+
# embedded rpx login (via iframe)
|
73
|
+
# options: :width, :height, :language, :flags, :api_version, :default_provider
|
74
|
+
def embed_code(subdomain, url, options={})
|
75
|
+
options = {:width => '400', :height => '240'}.merge(options)
|
76
|
+
<<-EOF
|
77
|
+
<iframe src="#{Api.host(subdomain)}/openid/embed?#{embed_params(url, options)}"
|
78
|
+
scrolling="no" frameBorder="no" style="width:#{options[:width]}px;height:#{options[:height]}px;">
|
79
|
+
</iframe>
|
80
|
+
EOF
|
81
|
+
end
|
82
|
+
|
83
|
+
# popup window for rpx login
|
84
|
+
# options: :language, :flags, :unobtrusive, :api_version, :default_provider
|
85
|
+
def popup_code(text, subdomain, url, options = {})
|
86
|
+
if options[:unobtrusive]
|
87
|
+
unobtrusive_popup_code(text, subdomain, url, options)
|
88
|
+
else
|
89
|
+
obtrusive_popup_code(text, subdomain, url, options)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# javascript for popup
|
94
|
+
# only needed in combination with popup_code(x,y,z, :unobtrusive => true)
|
95
|
+
def popup_source(subdomain, url, options={})
|
96
|
+
<<-EOF
|
97
|
+
<script src="#{Api.host}/openid/v#{extract_version(options)}/widget" type="text/javascript"></script>
|
98
|
+
<script type="text/javascript">
|
99
|
+
//<![CDATA[
|
100
|
+
RPXNOW.token_url = '#{url}';
|
101
|
+
RPXNOW.realm = '#{subdomain}';
|
102
|
+
RPXNOW.overlay = true;
|
103
|
+
RPXNOW.ssl = #{ssl};
|
104
|
+
#{ "RPXNOW.language_preference = '#{options[:language]}';" if options[:language] }
|
105
|
+
#{ "RPXNOW.default_provider = '#{options[:default_provider]}';" if options[:default_provider] }
|
106
|
+
#{ "RPXNOW.flags = '#{options[:flags]}';" if options[:flags] }
|
107
|
+
//]]>
|
108
|
+
</script>
|
109
|
+
EOF
|
110
|
+
end
|
111
|
+
|
112
|
+
# url for unobtrusive popup window
|
113
|
+
# options: :language, :flags, :api_version, :default_provider
|
114
|
+
def popup_url(subdomain, url, options={})
|
115
|
+
"#{Api.host(subdomain)}/openid/v#{extract_version(options)}/signin?#{embed_params(url, options)}"
|
116
|
+
end
|
117
|
+
|
118
|
+
def extract_version(options)
|
119
|
+
options[:api_version] || api_version
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def self.embed_params(url, options)
|
125
|
+
{
|
126
|
+
:token_url => CGI::escape( url ),
|
127
|
+
:language_preference => options[:language],
|
128
|
+
:flags => options[:flags],
|
129
|
+
:default_provider => options[:default_provider]
|
130
|
+
}.map{|k,v| "#{k}=#{v}" if v}.compact.join('&')
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.parse_user_data(response, options)
|
134
|
+
user_data = response['profile']
|
135
|
+
data = {}
|
136
|
+
data[:identifier] = user_data['identifier']
|
137
|
+
data[:email] = user_data['verifiedEmail'] || user_data['email']
|
138
|
+
data[:username] = user_data['preferredUsername'] || data[:email].to_s.sub(/@.*/,'')
|
139
|
+
data[:name] = user_data['displayName'] || data[:username]
|
140
|
+
data[:id] = user_data['primaryKey'] unless user_data['primaryKey'].to_s.empty?
|
141
|
+
(options[:additional] || []).each do |key|
|
142
|
+
if key == :raw
|
143
|
+
data[key] = user_data
|
144
|
+
else
|
145
|
+
data[key] = user_data[key.to_s]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
data
|
149
|
+
end
|
150
|
+
|
151
|
+
def unobtrusive_popup_code(text, subdomain, url, options={})
|
152
|
+
%Q(<a class="rpxnow" href="#{popup_url(subdomain, url, options)}">#{text}</a>)
|
153
|
+
end
|
154
|
+
|
155
|
+
def obtrusive_popup_code(text, subdomain, url, options = {})
|
156
|
+
unobtrusive_popup_code(text, subdomain, url, options) +
|
157
|
+
popup_source(subdomain, url, options)
|
158
|
+
end
|
159
|
+
|
160
|
+
class ServerError < RuntimeError; end #backwards compatibility / catch all
|
161
|
+
class ApiError < ServerError; end
|
162
|
+
class ServiceUnavailableError < ServerError; end
|
163
|
+
end
|
data/lib/rpx_now/api.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module RPXNow
|
6
|
+
# low-level interaction with rpxnow.com api
|
7
|
+
# - send requests
|
8
|
+
# - parse response
|
9
|
+
# - handle server errors
|
10
|
+
class Api
|
11
|
+
HOST = 'rpxnow.com'
|
12
|
+
SSL_CERT = File.join(File.dirname(__FILE__), '..', '..', 'certs', 'ssl_cert.pem')
|
13
|
+
|
14
|
+
def self.call(method, data)
|
15
|
+
data = data.dup
|
16
|
+
version = RPXNow.extract_version(data)
|
17
|
+
data.delete(:api_version)
|
18
|
+
|
19
|
+
path = "/api/v#{version}/#{method}"
|
20
|
+
response = request(path, {:apiKey => RPXNow.api_key}.merge(data))
|
21
|
+
parse_response(response)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.host(realm=nil)
|
25
|
+
protocol = RPXNow.ssl ? 'https' : 'http'
|
26
|
+
domain =
|
27
|
+
if realm.nil?
|
28
|
+
Api::HOST
|
29
|
+
elsif realm.include?('.')
|
30
|
+
realm
|
31
|
+
else
|
32
|
+
"#{realm}.#{Api::HOST}"
|
33
|
+
end
|
34
|
+
"#{protocol}://#{domain}"
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def self.request(path, data)
|
40
|
+
client.request(request_object(path, data))
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.request_object(path, data)
|
44
|
+
request = Net::HTTP::Post.new(path)
|
45
|
+
request.form_data = stringify_keys(data)
|
46
|
+
request
|
47
|
+
end
|
48
|
+
|
49
|
+
# symbol keys -> string keys
|
50
|
+
# because of ruby 1.9.x bug in Net::HTTP
|
51
|
+
# http://redmine.ruby-lang.org/issues/show/1351
|
52
|
+
def self.stringify_keys(hash)
|
53
|
+
hash.map{|k,v| [k.to_s,v]}
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.client
|
57
|
+
client = Net::HTTP.new(HOST, 443)
|
58
|
+
client.use_ssl = true
|
59
|
+
client.ca_file = SSL_CERT
|
60
|
+
client.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
61
|
+
client.verify_depth = 5
|
62
|
+
client
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.parse_response(response)
|
66
|
+
if response.code.to_i >= 400
|
67
|
+
raise ServiceUnavailableError, "The RPX service is temporarily unavailable. (4XX)"
|
68
|
+
else
|
69
|
+
result = JSON.parse(response.body)
|
70
|
+
return result unless result['err']
|
71
|
+
|
72
|
+
code = result['err']['code']
|
73
|
+
if code == -1
|
74
|
+
raise ServiceUnavailableError, "The RPX service is temporarily unavailable."
|
75
|
+
else
|
76
|
+
raise ApiError, "Got error: #{result['err']['msg']} (code: #{code}), HTTP status: #{response.code}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module RPXNow
|
2
|
+
# Makes returned contacts feel like a array
|
3
|
+
class ContactsCollection < Array
|
4
|
+
def initialize(list)
|
5
|
+
@raw = list
|
6
|
+
@additional_info = list.reject{|k,v|k=='entry'}
|
7
|
+
list['entry'].each{|item| self << parse_data(item)}
|
8
|
+
end
|
9
|
+
|
10
|
+
def additional_info;@additional_info;end
|
11
|
+
def raw;@raw;end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def parse_data(entry)
|
16
|
+
entry['emails'] = (entry['emails'] ? entry['emails'].map{|email| email['value']} : [])
|
17
|
+
entry
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module RPXNow
|
2
|
+
class UserProxy
|
3
|
+
def initialize(id)
|
4
|
+
@id = id
|
5
|
+
end
|
6
|
+
|
7
|
+
def identifiers
|
8
|
+
RPXNow.mappings(@id)
|
9
|
+
end
|
10
|
+
|
11
|
+
def map(identifier)
|
12
|
+
RPXNow.map(identifier, @id)
|
13
|
+
end
|
14
|
+
|
15
|
+
def unmap(identifier)
|
16
|
+
RPXNow.unmap(identifier, @id)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
{
|
2
|
+
"response": {
|
3
|
+
"itemsPerPage": 5,
|
4
|
+
"totalResults": 5,
|
5
|
+
"entry": [
|
6
|
+
{
|
7
|
+
"displayName": "Bob Johnson",
|
8
|
+
"emails": [
|
9
|
+
{
|
10
|
+
"type": "other",
|
11
|
+
"value": "bob@example.com"
|
12
|
+
}
|
13
|
+
]
|
14
|
+
},
|
15
|
+
{
|
16
|
+
"displayName": "Cindy Smith",
|
17
|
+
"emails": [
|
18
|
+
{
|
19
|
+
"type": "other",
|
20
|
+
"value": "cindy.smith@example.com"
|
21
|
+
}
|
22
|
+
]
|
23
|
+
},
|
24
|
+
{
|
25
|
+
"displayName": "Fred Williams",
|
26
|
+
"emails": [
|
27
|
+
{
|
28
|
+
"type": "other",
|
29
|
+
"value": "fred.williams@example.com"
|
30
|
+
},
|
31
|
+
{
|
32
|
+
"type": "other",
|
33
|
+
"value": "fred@example.com"
|
34
|
+
}
|
35
|
+
]
|
36
|
+
},
|
37
|
+
{
|
38
|
+
"emails": [
|
39
|
+
{
|
40
|
+
"type": "other",
|
41
|
+
"value": "j.lewis@example.com"
|
42
|
+
}
|
43
|
+
]
|
44
|
+
},
|
45
|
+
{
|
46
|
+
"displayName": "Mary Jones",
|
47
|
+
"emails": [
|
48
|
+
{
|
49
|
+
"type": "other",
|
50
|
+
"value": "mary.jones@example.com"
|
51
|
+
}
|
52
|
+
]
|
53
|
+
}
|
54
|
+
],
|
55
|
+
"startIndex": 1
|
56
|
+
},
|
57
|
+
"stat": "ok"
|
58
|
+
}
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec/spec_helper'
|
2
|
+
|
3
|
+
describe RPXNow do
|
4
|
+
describe :mapping_integration do
|
5
|
+
before do
|
6
|
+
@k1 = 'http://test.myopenid.com'
|
7
|
+
RPXNow.unmap(@k1, 1)
|
8
|
+
@k2 = 'http://test-2.myopenid.com'
|
9
|
+
RPXNow.unmap(@k2, 1)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "has no mappings when nothing was mapped" do
|
13
|
+
RPXNow.mappings(1).should == []
|
14
|
+
end
|
15
|
+
|
16
|
+
it "unmaps mapped keys" do
|
17
|
+
RPXNow.map(@k2, 1)
|
18
|
+
RPXNow.unmap(@k2, 1)
|
19
|
+
RPXNow.mappings(1).should == []
|
20
|
+
end
|
21
|
+
|
22
|
+
it "maps keys to a primary key and then retrieves them" do
|
23
|
+
RPXNow.map(@k1, 1)
|
24
|
+
RPXNow.map(@k2, 1)
|
25
|
+
RPXNow.mappings(1).sort.should == [@k2,@k1]
|
26
|
+
end
|
27
|
+
|
28
|
+
it "does not add duplicate mappings" do
|
29
|
+
RPXNow.map(@k1, 1)
|
30
|
+
RPXNow.map(@k1, 1)
|
31
|
+
RPXNow.mappings(1).should == [@k1]
|
32
|
+
end
|
33
|
+
|
34
|
+
it "finds all mappings" do
|
35
|
+
RPXNow.map(@k1, 1)
|
36
|
+
RPXNow.map(@k2, 2)
|
37
|
+
RPXNow.all_mappings.sort.should == [["1", ["http://test.myopenid.com"]], ["2", ["http://test-2.myopenid.com"]]]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'spec/spec_helper'
|
2
|
+
|
3
|
+
describe RPXNow::Api do
|
4
|
+
describe 'ssl cert' do
|
5
|
+
it "has an absolute path" do
|
6
|
+
RPXNow::Api::SSL_CERT[0..0].should == File.expand_path( RPXNow::Api::SSL_CERT )[0..0] # start with '/' on *nix, drive letter on win
|
7
|
+
end
|
8
|
+
|
9
|
+
it "exists" do
|
10
|
+
File.read(RPXNow::Api::SSL_CERT).to_s.should_not be_empty
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe :host do
|
15
|
+
after do
|
16
|
+
RPXNow.ssl = true
|
17
|
+
end
|
18
|
+
|
19
|
+
context "when the realm is a domain" do
|
20
|
+
it "returns the domain itself" do
|
21
|
+
RPXNow::Api.host("login.example.com").should == 'https://login.example.com'
|
22
|
+
end
|
23
|
+
|
24
|
+
it "honors the ssl setting" do
|
25
|
+
RPXNow.ssl = false
|
26
|
+
RPXNow::Api.host("login.example.com").should == 'http://login.example.com'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context "when the realm is a subdomain" do
|
31
|
+
it "returns the qualified rpx domain" do
|
32
|
+
RPXNow::Api.host("example").should == 'https://example.rpxnow.com'
|
33
|
+
end
|
34
|
+
|
35
|
+
it "honors the ssl setting" do
|
36
|
+
RPXNow.ssl = false
|
37
|
+
RPXNow::Api.host("example").should == 'http://example.rpxnow.com'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context "when no realm is provided" do
|
42
|
+
it "returns the rpx domain" do
|
43
|
+
RPXNow::Api.host.should == 'https://rpxnow.com'
|
44
|
+
end
|
45
|
+
|
46
|
+
it "honors the ssl setting" do
|
47
|
+
RPXNow.ssl = false
|
48
|
+
RPXNow::Api.host.should == 'http://rpxnow.com'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe :parse_response do
|
54
|
+
it "parses json when status is ok" do
|
55
|
+
response = mock(:code=>'200', :body=>%Q({"stat":"ok","data":"xx"}))
|
56
|
+
RPXNow::Api.send(:parse_response, response)['data'].should == "xx"
|
57
|
+
end
|
58
|
+
|
59
|
+
it "raises when there is a communication error" do
|
60
|
+
response = stub(:code=>'200', :body=>%Q({"err":"wtf","stat":"ok"}))
|
61
|
+
lambda{
|
62
|
+
RPXNow::Api.send(:parse_response,response)
|
63
|
+
}.should raise_error(RPXNow::ApiError)
|
64
|
+
end
|
65
|
+
|
66
|
+
it "raises when service has downtime" do
|
67
|
+
response = stub(:code=>'200', :body=>%Q({"err":{"code":-1},"stat":"ok"}))
|
68
|
+
lambda{
|
69
|
+
RPXNow::Api.send(:parse_response,response)
|
70
|
+
}.should raise_error(RPXNow::ServiceUnavailableError)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "raises when service is down" do
|
74
|
+
response = stub(:code=>'400',:body=>%Q({"stat":"err"}))
|
75
|
+
lambda{
|
76
|
+
RPXNow::Api.send(:parse_response,response)
|
77
|
+
}.should raise_error(RPXNow::ServiceUnavailableError)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe :request_object do
|
82
|
+
it "converts symbols to string keys" do
|
83
|
+
mock = ''
|
84
|
+
mock.should_receive(:form_data=).with([['symbol', 'value']])
|
85
|
+
Net::HTTP::Post.should_receive(:new).and_return(mock)
|
86
|
+
RPXNow::Api.send(:request_object, 'something', :symbol=>'value')
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|