Empact-rpx_now 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ #Needed to load rpx_now when used as Rails plugin
2
+ require 'rpx_now'
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('&amp;')
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
@@ -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,9 @@
1
+ require 'rpx_now/user_proxy'
2
+
3
+ module RPXNow
4
+ module UserIntegration
5
+ def rpx
6
+ RPXNow::UserProxy.new(id)
7
+ end
8
+ end
9
+ 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