sundawg_contacts 0.0.1

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/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