sundawg_contacts 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2009 Mislav Marohnić
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Mislav Marohnić
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Manifest ADDED
@@ -0,0 +1,30 @@
1
+ LICENSE
2
+ MIT-LICENSE
3
+ Manifest
4
+ README.rdoc
5
+ Rakefile
6
+ lib/config/contacts.yml
7
+ lib/contacts.rb
8
+ lib/contacts/flickr.rb
9
+ lib/contacts/google.rb
10
+ lib/contacts/version.rb
11
+ lib/contacts/windows_live.rb
12
+ lib/contacts/yahoo.rb
13
+ spec/contact_spec.rb
14
+ spec/feeds/contacts.yml
15
+ spec/feeds/flickr/auth.getFrob.xml
16
+ spec/feeds/flickr/auth.getToken.xml
17
+ spec/feeds/google-many.xml
18
+ spec/feeds/google-single.xml
19
+ spec/feeds/wl_contacts.xml
20
+ spec/feeds/yh_contacts.txt
21
+ spec/feeds/yh_credential.xml
22
+ spec/flickr/auth_spec.rb
23
+ spec/gmail/auth_spec.rb
24
+ spec/gmail/fetching_spec.rb
25
+ spec/rcov.opts
26
+ spec/spec.opts
27
+ spec/spec_helper.rb
28
+ spec/windows_live/windows_live_spec.rb
29
+ spec/yahoo/yahoo_spec.rb
30
+ vendor/windows_live_login.rb
data/README.rdoc ADDED
@@ -0,0 +1,47 @@
1
+ == Basic usage instructions
2
+
3
+ Fetch users' contact lists from your web application without asking them to
4
+ provide their passwords.
5
+
6
+ First, register[http://code.google.com/apis/accounts/docs/RegistrationForWebAppsAuto.html]
7
+ your application's domain. Then make users follow this URL:
8
+
9
+ Contacts::Google.authentication_url('http://mysite.com/invite')
10
+
11
+ They will authenticate on Google and it will send them back to the URL
12
+ provided. Google will add a token GET parameter to the query part of the URL.
13
+ Use that token in the next step:
14
+
15
+ gmail = Contacts::Google.new(params[:token])
16
+ gmail.contacts
17
+ # => [#<Contact 1>, #<Contact 2>, ...]
18
+
19
+ The resulting Contacts::Contact objects have `name` and `email` properties.
20
+
21
+ Read more in Contacts::Google. I plan to support more APIs (Microsoft Live, for
22
+ starters); feel free to contribute.
23
+
24
+ Author: <b>Mislav Marohnić</b> (mislav.marohnic@gmail.com)
25
+
26
+ == Documentation auto-generated from specifications
27
+
28
+ Contacts::Google.authentication_url
29
+ - generates a URL for target with default parameters
30
+ - should handle boolean parameters
31
+ - skips parameters that have nil value
32
+ - should be able to exchange one-time for session token
33
+
34
+ Contacts::Google
35
+ - fetches contacts feed via HTTP GET
36
+ - handles a normal response body
37
+ - handles gzipped response
38
+ - raises a FetchingError when something goes awry
39
+ - parses the resulting feed into name/email pairs
40
+ - parses a complex feed into name/email pairs
41
+ - makes modification time available after parsing
42
+
43
+ Contacts::Google GET query parameter handling
44
+ - abstracts ugly parameters behind nicer ones
45
+ - should have implicit :descending with :order
46
+ - should have default :limit of 200
47
+ - should skip nil values in parameters
data/Rakefile ADDED
@@ -0,0 +1,68 @@
1
+ require 'spec/rake/spectask'
2
+ require 'rake/rdoctask'
3
+ require 'rubygems'
4
+ require 'rake'
5
+ require 'echoe'
6
+
7
+ task :default => :spec
8
+
9
+ spec_opts = 'spec/spec.opts'
10
+ spec_glob = FileList['spec/**/*_spec.rb']
11
+ libs = ['lib', 'spec', 'vendor/fakeweb/lib']
12
+
13
+ desc 'Run all specs in spec directory'
14
+ Spec::Rake::SpecTask.new(:spec) do |t|
15
+ t.libs = libs
16
+ t.spec_opts = ['--options', spec_opts]
17
+ t.spec_files = spec_glob
18
+ # t.warning = true
19
+ end
20
+
21
+ namespace :spec do
22
+ desc 'Analyze spec coverage with RCov'
23
+ Spec::Rake::SpecTask.new(:rcov) do |t|
24
+ t.libs = libs
25
+ t.spec_files = spec_glob
26
+ t.spec_opts = ['--options', spec_opts]
27
+ t.rcov = true
28
+ t.rcov_opts = lambda do
29
+ IO.readlines('spec/rcov.opts').map { |l| l.chomp.split(" ") }.flatten
30
+ end
31
+ end
32
+
33
+ desc 'Print Specdoc for all specs'
34
+ Spec::Rake::SpecTask.new(:doc) do |t|
35
+ t.libs = libs
36
+ t.spec_opts = ['--format', 'specdoc', '--dry-run']
37
+ t.spec_files = spec_glob
38
+ end
39
+
40
+ desc 'Generate HTML report'
41
+ Spec::Rake::SpecTask.new(:html) do |t|
42
+ t.libs = libs
43
+ t.spec_opts = ['--format', 'html:doc/spec.html', '--diff']
44
+ t.spec_files = spec_glob
45
+ t.fail_on_error = false
46
+ end
47
+ end
48
+
49
+ desc 'Generate RDoc documentation'
50
+ Rake::RDocTask.new(:rdoc) do |rdoc|
51
+ rdoc.rdoc_files.add ['README.rdoc', 'MIT-LICENSE', 'lib/**/*.rb']
52
+ rdoc.main = 'README.rdoc'
53
+ rdoc.title = 'Ruby Contacts library'
54
+
55
+ rdoc.rdoc_dir = 'doc'
56
+ rdoc.options << '--inline-source'
57
+ rdoc.options << '--charset=UTF-8'
58
+ end
59
+
60
+ desc 'Generate Ruby gem'
61
+ Echoe.new('sundawg_contacts', '0.0.1') do |p|
62
+ p.description = "Project fork of Mislav Contacts to support signed GData protocol."
63
+ p.url = "http://github.com/SunDawg/contacts"
64
+ p.author = "Christopher Sun"
65
+ p.email = "christopher.sun@gmail.com"
66
+ p.ignore_pattern = ["tmp/*", "script/*"]
67
+ p.development_dependencies = ['fakeweb >=1.2.7']
68
+ end
@@ -0,0 +1,10 @@
1
+ windows_live:
2
+ appid: your_app_id
3
+ secret: your_app_secret_key
4
+ security_algorithm: wsignin1.0
5
+ return_url: http://yourserver.com/your_return_url
6
+ policy_url: http://yourserver.com/you_policy_url
7
+
8
+ yahoo:
9
+ appid: your_app_id
10
+ secret: your_shared_secret
data/lib/contacts.rb ADDED
@@ -0,0 +1,46 @@
1
+ require 'contacts/version'
2
+
3
+ module Contacts
4
+
5
+ Identifier = 'Ruby Contacts v' + VERSION::STRING
6
+
7
+ # An object that represents a single contact
8
+ class Contact
9
+ attr_reader :name, :username, :emails
10
+
11
+ def initialize(email, name = nil, username = nil)
12
+ @emails = []
13
+ @emails << email if email
14
+ @name = name
15
+ @username = username
16
+ end
17
+
18
+ def email
19
+ @emails.first
20
+ end
21
+
22
+ def inspect
23
+ %!#<Contacts::Contact "#{name}"#{email ? " (#{email})" : ''}>!
24
+ end
25
+ end
26
+
27
+ def self.verbose?
28
+ 'irb' == $0
29
+ end
30
+
31
+ class Error < StandardError
32
+ end
33
+
34
+ class TooManyRedirects < Error
35
+ attr_reader :response, :location
36
+
37
+ MAX_REDIRECTS = 2
38
+
39
+ def initialize(response)
40
+ @response = response
41
+ @location = @response['Location']
42
+ super "exceeded maximum of #{MAX_REDIRECTS} redirects (Location: #{location})"
43
+ end
44
+ end
45
+
46
+ end
@@ -0,0 +1,133 @@
1
+ require 'contacts'
2
+
3
+ require 'rubygems'
4
+ require 'hpricot'
5
+ require 'md5'
6
+ require 'cgi'
7
+ require 'time'
8
+ require 'zlib'
9
+ require 'stringio'
10
+ require 'net/http'
11
+
12
+ module Contacts
13
+
14
+ class Flickr
15
+ DOMAIN = 'api.flickr.com'
16
+ ServicesPath = '/services/rest/'
17
+
18
+ def self.frob_url(key, secret)
19
+ url_for(:api_key => key, :secret => secret, :method => 'flickr.auth.getFrob')
20
+ end
21
+
22
+ def self.frob_from_response(response)
23
+ doc = Hpricot::XML response.body
24
+ doc.at('frob').inner_text
25
+ end
26
+
27
+ def self.authentication_url_for_frob(frob, key, secret)
28
+ params = { :api_key => key, :secret => secret, :perms => 'read', :frob => frob }
29
+ 'http://www.flickr.com/services/auth/?' + query_string(params)
30
+ end
31
+
32
+ def self.authentication_url(key, secret)
33
+ response = http_start do |flickr|
34
+ flickr.get(frob_url(key, secret))
35
+ end
36
+ authentication_url_for_frob(frob_from_response(response), key, secret)
37
+ end
38
+
39
+ def self.token_url(key, secret, frob)
40
+ params = { :api_key => key, :secret => secret, :frob => frob, :method => 'flickr.auth.getToken' }
41
+ url_for(params)
42
+ end
43
+
44
+ def self.get_token_from_frob(key, secret, frob)
45
+ response = http_start do |flickr|
46
+ flickr.get(token_url(key, secret, frob))
47
+ end
48
+ doc = Hpricot::XML response.body
49
+ doc.at('token').inner_text
50
+ end
51
+
52
+ private
53
+ # Use the key-sorted version of the parameters to construct
54
+ # a string, to which the secret is prepended.
55
+
56
+ def self.sort_params(params)
57
+ params.sort do |a,b|
58
+ a.to_s <=> b.to_s
59
+ end
60
+ end
61
+
62
+ def self.string_to_sign(params, secret)
63
+ string_to_sign = secret + sort_params(params).inject('') do |str, pair|
64
+ key, value = pair
65
+ str + key.to_s + value.to_s
66
+ end
67
+ end
68
+
69
+ # Get the MD5 digest of the string to sign
70
+ def self.get_signature(params, secret)
71
+ ::Digest::MD5.hexdigest(string_to_sign(params, secret))
72
+ end
73
+
74
+ def self.query_string(params)
75
+ secret = params.delete(:secret)
76
+ params[:api_sig] = get_signature(params, secret)
77
+
78
+ params.inject([]) do |arr, pair|
79
+ key, value = pair
80
+ arr << "#{key}=#{value}"
81
+ end.join('&')
82
+ end
83
+
84
+ def self.url_for(params)
85
+ ServicesPath + '?' + query_string(params)
86
+ end
87
+
88
+ def self.http_start(ssl = false)
89
+ port = ssl ? Net::HTTP::https_default_port : Net::HTTP::http_default_port
90
+ http = Net::HTTP.new(DOMAIN, port)
91
+ redirects = 0
92
+ if ssl
93
+ http.use_ssl = true
94
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
95
+ end
96
+ http.start
97
+
98
+ begin
99
+ response = yield(http)
100
+
101
+ loop do
102
+ inspect_response(response) if Contacts::verbose?
103
+
104
+ case response
105
+ when Net::HTTPSuccess
106
+ break response
107
+ when Net::HTTPRedirection
108
+ if redirects == TooManyRedirects::MAX_REDIRECTS
109
+ raise TooManyRedirects.new(response)
110
+ end
111
+ location = URI.parse response['Location']
112
+ puts "Redirected to #{location}"
113
+ response = http.get(location.path)
114
+ redirects += 1
115
+ else
116
+ response.error!
117
+ end
118
+ end
119
+ ensure
120
+ http.finish
121
+ end
122
+ end
123
+
124
+ def self.inspect_response(response, out = $stderr)
125
+ out.puts response.inspect
126
+ for name, value in response
127
+ out.puts "#{name}: #{value}"
128
+ end
129
+ out.puts "----\n#{response.body}\n----" unless response.body.empty?
130
+ end
131
+ end
132
+
133
+ end
@@ -0,0 +1,353 @@
1
+ require 'contacts'
2
+
3
+ require 'rubygems'
4
+ require 'hpricot'
5
+ require 'cgi'
6
+ require 'time'
7
+ require 'zlib'
8
+ require 'stringio'
9
+ require 'net/http'
10
+ require 'net/https'
11
+ require 'base64'
12
+
13
+ module Contacts
14
+ # == Fetching Google Contacts
15
+ #
16
+ # First, get the user to follow the following URL:
17
+ #
18
+ # Contacts::Google.authentication_url('http://mysite.com/invite')
19
+ #
20
+ # After he authenticates successfully to Google, it will redirect him back to the target URL
21
+ # (specified as argument above) and provide the token GET parameter. Use it to create a
22
+ # new instance of this class and request the contact list:
23
+ #
24
+ # gmail = Contacts::Google.new(params[:token])
25
+ # contacts = gmail.contacts
26
+ # #-> [ ['Fitzgerald', 'fubar@gmail.com', 'fubar@example.com'],
27
+ # ['William Paginate', 'will.paginate@gmail.com'], ...
28
+ # ]
29
+ #
30
+ # == Storing a session token
31
+ #
32
+ # The basic token that you will get after the user has authenticated on Google is valid
33
+ # for <b>only one request</b>. However, you can specify that you want a session token which
34
+ # doesn't expire:
35
+ #
36
+ # Contacts::Google.authentication_url('http://mysite.com/invite', :session => true)
37
+ #
38
+ # When the user authenticates, he will be redirected back with a token that can be exchanged
39
+ # for a session token with the following method:
40
+ #
41
+ # token = Contacts::Google.sesion_token(params[:token])
42
+ #
43
+ # Now you have a permanent token. Store it with other user data so you can query the API
44
+ # on his behalf without him having to authenticate on Google each time.
45
+ class Google
46
+ DOMAIN = 'www.google.com'
47
+ AuthSubPath = '/accounts/AuthSub' # all variants go over HTTPS
48
+ ClientLogin = '/accounts/ClientLogin'
49
+ FeedsPath = '/m8/feeds/contacts/'
50
+
51
+ # default options for #authentication_url
52
+ def self.authentication_url_options
53
+ @authentication_url_options ||= {
54
+ :scope => "https://#{DOMAIN}#{FeedsPath}",
55
+ :secure => false,
56
+ :session => false
57
+ }
58
+ end
59
+
60
+ # default options for #client_login
61
+ def self.client_login_options
62
+ @client_login_options ||= {
63
+ :accountType => 'GOOGLE',
64
+ :service => 'cp',
65
+ :source => 'Contacts-Ruby'
66
+ }
67
+ end
68
+
69
+ # URL to Google site where user authenticates. Afterwards, Google redirects to your
70
+ # site with the URL specified as +target+.
71
+ #
72
+ # Options are:
73
+ # * <tt>:scope</tt> -- the AuthSub scope in which the resulting token is valid
74
+ # (default: "http://www.google.com/m8/feeds/contacts/")
75
+ # * <tt>:secure</tt> -- boolean indicating whether the token will be secure. Only available
76
+ # for registered domains.
77
+ # (default: false)
78
+ # * <tt>:session</tt> -- boolean indicating if the token can be exchanged for a session token
79
+ # (default: false)
80
+ def self.authentication_url(target, options = {})
81
+ params = authentication_url_options.merge(options)
82
+ if key = params.delete(:key)
83
+ params[:secure] = true
84
+ set_private_key(key)
85
+ end
86
+ params[:next] = target
87
+ query = query_string(params)
88
+ "https://#{DOMAIN}#{AuthSubPath}Request?#{query}"
89
+ end
90
+
91
+ # Sets the private key for a secure AuthSub request. +key+ may be an IO, String or
92
+ # OpenSSL::PKey::RSA.
93
+ # Stolen from http://github.com/stuart/google-authsub/lib/googleauthsub.rb
94
+ def self.set_private_key(key)
95
+ case key
96
+ when OpenSSL::PKey::RSA
97
+ @@pkey = key
98
+ when File
99
+ @@pkey = OpenSSL::PKey::RSA.new(key.read)
100
+ when String
101
+ @@pkey = OpenSSL::PKey::RSA.new(key)
102
+ else
103
+ raise "Private Key in wrong format. Require IO, String or OpenSSL::PKey::RSA, you gave me #{key.class}"
104
+ end
105
+ end
106
+
107
+ # Unsets the private key. Only used for test teardowns.
108
+ def self.unset_private_key
109
+ @@pkey = nil
110
+ end
111
+
112
+ # Makes an HTTPS request to exchange the given token with a session one. Session
113
+ # tokens never expire, so you can store them in the database alongside user info.
114
+ #
115
+ # Returns the new token as string or nil if the parameter couldn't be found in response
116
+ # body.
117
+ def self.session_token(token)
118
+ response = http_start(true) do |google|
119
+ uri = AuthSubPath + 'SessionToken'
120
+ header = authorization_header(token, false, uri)
121
+ google.get(uri, header)
122
+ end
123
+ pair = response.body.split(/\n/).detect { |p| p.index('Token=') == 0 }
124
+ pair.split('=').last if pair
125
+ end
126
+
127
+ # Alternative to AuthSub: using email and password.
128
+ def self.client_login(email, password)
129
+ response = http_start do |google|
130
+ query = query_string(client_login_options.merge(:Email => email, :Passwd => password))
131
+ google.post(ClientLogin, query)
132
+ end
133
+
134
+ pair = response.body.split(/\n/).detect { |p| p.index('Auth=') == 0 }
135
+ pair.split('=').last if pair
136
+ end
137
+
138
+ attr_reader :user, :token, :headers, :author
139
+ attr_accessor :projection
140
+
141
+ # A token is required here. By default, an AuthSub token from
142
+ # Google is one-time only, which means you can only make a single request with it.
143
+ def initialize(token, user_id = 'default', client = false)
144
+ @user = user_id.to_s
145
+ @token = token.to_s
146
+ @client = client
147
+ @headers = {
148
+ 'Accept-Encoding' => 'gzip',
149
+ 'User-Agent' => Identifier + ' (gzip)'
150
+ }.update(self.class.authorization_header(@token, client))
151
+ @projection = 'thin'
152
+ end
153
+
154
+ def get(params) # :nodoc:
155
+ self.class.http_start(true) do |google|
156
+ path = FeedsPath + CGI.escape(@user)
157
+ google_params = translate_parameters(params)
158
+ query = self.class.query_string(google_params)
159
+ uri = "#{path}/#{@projection}?#{query}"
160
+ headers = @headers.update(self.class.authorization_header(@token, @client, uri))
161
+ google.get(uri, headers)
162
+ end
163
+ end
164
+
165
+ # Timestamp of last update. This value is available only after the XML
166
+ # document has been parsed; for instance after fetching the contact list.
167
+ def updated_at
168
+ @updated_at ||= Time.parse @updated_string if @updated_string
169
+ end
170
+
171
+ # Timestamp of last update as it appeared in the XML document
172
+ def updated_at_string
173
+ @updated_string
174
+ end
175
+
176
+ # Fetches, parses and returns the contact list.
177
+ #
178
+ # ==== Options
179
+ # * <tt>:limit</tt> -- use a large number to fetch a bigger contact list (default: 200)
180
+ # * <tt>:offset</tt> -- 0-based value, can be used for pagination
181
+ # * <tt>:order</tt> -- currently the only value support by Google is "lastmodified"
182
+ # * <tt>:descending</tt> -- boolean
183
+ # * <tt>:updated_after</tt> -- string or time-like object, use to only fetch contacts
184
+ # that were updated after this date
185
+ def contacts(options = {})
186
+ params = { :limit => 200 }.update(options)
187
+ response = get(params)
188
+ parse_contacts response_body(response)
189
+ end
190
+
191
+ # Fetches contacts using multiple API calls when necessary
192
+ def all_contacts(options = {}, chunk_size = 200)
193
+ in_chunks(options, :contacts, chunk_size)
194
+ end
195
+
196
+ protected
197
+
198
+ def in_chunks(options, what, chunk_size)
199
+ returns = []
200
+ offset = 0
201
+
202
+ begin
203
+ chunk = send(what, options.merge(:offset => offset, :limit => chunk_size))
204
+ returns.push(*chunk)
205
+ offset += chunk_size
206
+ end while chunk.size == chunk_size
207
+
208
+ returns
209
+ end
210
+
211
+ def response_body(response)
212
+ unless response['Content-Encoding'] == 'gzip'
213
+ response.body
214
+ else
215
+ gzipped = StringIO.new(response.body)
216
+ Zlib::GzipReader.new(gzipped).read
217
+ end
218
+ end
219
+
220
+ def parse_contacts(body)
221
+ doc = Hpricot::XML body
222
+ contacts_found = []
223
+
224
+ if updated_node = doc.at('/feed/updated')
225
+ @updated_string = updated_node.inner_text
226
+ end
227
+
228
+ (doc / '/feed/entry').each do |entry|
229
+ email_nodes = entry / 'gd:email[@address]'
230
+
231
+ unless email_nodes.empty?
232
+ title_node = entry.at('/title')
233
+ name = title_node ? title_node.inner_text : nil
234
+ contact = Contact.new(nil, name)
235
+ contact.emails.concat email_nodes.map { |e| e['address'].to_s }
236
+ contacts_found << contact
237
+ end
238
+ end
239
+
240
+ entry = (doc / '/feed/author').first
241
+ @author = Contact.new(entry.at('/email').inner_text, entry.at('/name').inner_text) if entry
242
+
243
+ contacts_found
244
+ end
245
+
246
+ # Constructs a query string from a Hash object
247
+ def self.query_string(params)
248
+ params.inject([]) do |all, pair|
249
+ key, value = pair
250
+ unless value.nil?
251
+ value = case value
252
+ when TrueClass; '1'
253
+ when FalseClass; '0'
254
+ else value
255
+ end
256
+
257
+ all << "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
258
+ end
259
+ all
260
+ end.join('&')
261
+ end
262
+
263
+ def translate_parameters(params)
264
+ params.inject({}) do |all, pair|
265
+ key, value = pair
266
+ unless value.nil?
267
+ key = case key
268
+ when :limit
269
+ 'max-results'
270
+ when :offset
271
+ value = value.to_i + 1
272
+ 'start-index'
273
+ when :order
274
+ all['sortorder'] = 'descending' if params[:descending].nil?
275
+ 'orderby'
276
+ when :descending
277
+ value = value ? 'descending' : 'ascending'
278
+ 'sortorder'
279
+ when :updated_after
280
+ value = value.strftime("%Y-%m-%dT%H:%M:%S%Z") if value.respond_to? :strftime
281
+ 'updated-min'
282
+ else key
283
+ end
284
+
285
+ all[key] = value
286
+ end
287
+ all
288
+ end
289
+ end
290
+
291
+ def self.secure?
292
+ defined?(@@pkey) && !@@pkey.nil?
293
+ end
294
+
295
+ def self.authorization_header(token, client = false, uri = nil)
296
+ if client
297
+ { 'Authorization' => %(GoogleLogin auth="#{token}") }
298
+ elsif secure?
299
+ timestamp = Time.now.to_i
300
+ nonce = OpenSSL::BN.rand_range(2**64)
301
+ data = "GET https://#{DOMAIN}#{uri} #{timestamp} #{nonce}"
302
+ sig = @@pkey.sign(OpenSSL::Digest::SHA1.new, data)
303
+ sig = Base64.b64encode(sig).gsub(/\n/, '') #Base64 encode
304
+ { 'Authorization' => "AuthSub token=\"#{token}\" sigalg=\"rsa-sha1\" data=\"#{data}\" sig=\"#{sig}\"" }
305
+ else
306
+ { 'Authorization' => %(AuthSub token="#{token}") }
307
+ end
308
+ end
309
+
310
+ def self.http_start(ssl = true)
311
+ port = ssl ? Net::HTTP::https_default_port : Net::HTTP::http_default_port
312
+ http = Net::HTTP.new(DOMAIN, port)
313
+ redirects = 0
314
+ if ssl
315
+ http.use_ssl = true
316
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
317
+ end
318
+ http.start
319
+
320
+ begin
321
+ response = yield(http)
322
+
323
+ loop do
324
+ inspect_response(response) if Contacts::verbose?
325
+
326
+ case response
327
+ when Net::HTTPSuccess
328
+ break response
329
+ when Net::HTTPRedirection
330
+ if redirects == TooManyRedirects::MAX_REDIRECTS
331
+ raise TooManyRedirects.new(response)
332
+ end
333
+ location = URI.parse response['Location']
334
+ response = http.get(location.path)
335
+ redirects += 1
336
+ else
337
+ response.error!
338
+ end
339
+ end
340
+ ensure
341
+ http.finish
342
+ end
343
+ end
344
+
345
+ def self.inspect_response(response, out = $stderr)
346
+ out.puts response.body
347
+ out.puts response.inspect
348
+ for name, value in response
349
+ out.puts "#{name}: #{value}"
350
+ end
351
+ end
352
+ end
353
+ end