thehack-atom-tools 2.0.3

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/lib/atom/entry.rb ADDED
@@ -0,0 +1,134 @@
1
+ require "rexml/document"
2
+
3
+ require "atom/element"
4
+ require "atom/text"
5
+
6
+ module Atom
7
+ class Control < Atom::Element
8
+ attr_accessor :draft
9
+
10
+ is_element PP_NS, :control
11
+
12
+ on_parse [PP_NS, 'draft'] do |e,x|
13
+ e.set(:draft, x.text == 'yes')
14
+ end
15
+
16
+ on_build do |e,x|
17
+ unless (v = e.get(:draft)).nil?
18
+ el = e.append_elem(x, ['app', PP_NS], 'draft')
19
+ el.text = (v ? 'yes' : 'no')
20
+ end
21
+ end
22
+ end
23
+
24
+ module HasCategories
25
+ def HasCategories.included(klass)
26
+ klass.atom_elements :category, :categories, Atom::Category
27
+ end
28
+
29
+ # categorize the entry with each of an array or a space-separated
30
+ # string
31
+ def tag_with(tags, delimiter = ' ')
32
+ return if not tags or tags.empty?
33
+
34
+ tag_list = unless tags.is_a?(String)
35
+ tags
36
+ else
37
+ tags = tags.split(delimiter)
38
+ tags.map! { |t| t.strip }
39
+ tags.reject! { |t| t.empty? }
40
+ tags.uniq
41
+ end
42
+
43
+ tag_list.each do |tag|
44
+ unless categories.any? { |c| c.term == tag }
45
+ categories.new :term => tag
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ module HasLinks
52
+ def HasLinks.included(klass)
53
+ klass.atom_elements :link, :links, Atom::Link
54
+ end
55
+
56
+ def find_link(criteria)
57
+ self.links.find do |l|
58
+ criteria.all? { |k,v| l.send(k) == v }
59
+ end
60
+ end
61
+ end
62
+
63
+ # An individual entry in a feed. As an Atom::Element, it can be
64
+ # manipulated using accessors for each of its child elements. You
65
+ # should be able to set them using an instance of any class that
66
+ # makes sense
67
+ #
68
+ # Entries have the following children:
69
+ #
70
+ # id:: a universally unique IRI which permanently identifies the entry
71
+ # title:: a human-readable title (Atom::Text)
72
+ # content:: contains or links to the content of an entry (Atom::Content)
73
+ # rights:: information about rights held in and over an entry (Atom::Text)
74
+ # source:: the source feed's metadata (unimplemented)
75
+ # published:: a Time "early in the life cycle of an entry"
76
+ # updated:: the most recent Time an entry was modified in a way the publisher considers significant
77
+ # summary:: a summary, abstract or excerpt of an entry (Atom::Text)
78
+ #
79
+ # There are also +categories+, +links+, +authors+ and +contributors+,
80
+ # each of which is an Array of its respective type and can be used
81
+ # thusly:
82
+ #
83
+ # author = entry.authors.new :name => "Captain Kangaroo", :email => "kanga@example.net"
84
+ #
85
+ class Entry < Atom::Element
86
+ is_atom_element :entry
87
+
88
+ # the master list of standard children and the types they map to
89
+ atom_string :id
90
+
91
+ atom_element :title, Atom::Title
92
+ atom_element :summary, Atom::Summary
93
+ atom_element :content, Atom::Content
94
+
95
+ atom_element :rights, Atom::Rights
96
+
97
+ # element :source, Atom::Feed # XXX complicated, eg. serialization
98
+
99
+ atom_time :published
100
+ atom_time :updated
101
+ time ['app', PP_NS], :edited
102
+
103
+ atom_elements :author, :authors, Atom::Author
104
+ atom_elements :contributor, :contributors, Atom::Contributor
105
+
106
+ element ['app', PP_NS], :control, Atom::Control
107
+
108
+ include HasCategories
109
+ include HasLinks
110
+
111
+ atom_link :edit_url, :rel => 'edit'
112
+
113
+ def inspect # :nodoc:
114
+ "#<Atom::Entry id:'#{self.id}'>"
115
+ end
116
+
117
+ def draft
118
+ control and control.draft
119
+ end
120
+
121
+ alias :draft? :draft
122
+
123
+ def draft!
124
+ self.draft = true
125
+ end
126
+
127
+ def draft= is_draft
128
+ unless control
129
+ instance_variable_set '@control', Atom::Control.new
130
+ end
131
+ control.draft = is_draft
132
+ end
133
+ end
134
+ end
data/lib/atom/feed.rb ADDED
@@ -0,0 +1,223 @@
1
+ require "atom/element"
2
+ require "atom/text"
3
+ require "atom/entry"
4
+
5
+ require "atom/http"
6
+
7
+ module Atom
8
+ class FeedGone < RuntimeError # :nodoc:
9
+ end
10
+
11
+ # A feed of entries. As an Atom::Element, it can be manipulated using
12
+ # accessors for each of its child elements. You can set them with any
13
+ # object that makes sense; they will be returned in the types listed.
14
+ #
15
+ # Feeds have the following children:
16
+ #
17
+ # id:: a universally unique IRI which permanently identifies the feed
18
+ # title:: a human-readable title (Atom::Text)
19
+ # subtitle:: a human-readable description or subtitle (Atom::Text)
20
+ # updated:: the most recent Time the feed was modified in a way the publisher considers significant
21
+ # generator:: the agent used to generate a feed
22
+ # icon:: an IRI identifying an icon which visually identifies a feed (1:1 aspect ratio, looks OK small)
23
+ # logo:: an IRI identifying an image which visually identifies a feed (2:1 aspect ratio)
24
+ # rights:: rights held in and over a feed (Atom::Text)
25
+ #
26
+ # There are also +links+, +categories+, +authors+, +contributors+
27
+ # and +entries+, each of which is an Array of its respective type and
28
+ # can be used thusly:
29
+ #
30
+ # entry = feed.entries.new
31
+ # entry.title = "blah blah blah"
32
+ #
33
+ class Feed < Atom::Element
34
+ is_atom_element :feed
35
+
36
+ attr_reader :uri
37
+
38
+ # the Atom::Feed pointed to by link[@rel='previous']
39
+ attr_reader :prev
40
+ # the Atom::Feed pointed to by link[@rel='next']
41
+ attr_reader :next
42
+
43
+ # conditional get information from the last fetch
44
+ attr_reader :etag, :last_modified
45
+
46
+ atom_string :id
47
+ atom_element :title, Atom::Title
48
+ atom_element :subtitle, Atom::Subtitle
49
+
50
+ atom_time :updated
51
+
52
+ include HasLinks
53
+ include HasCategories
54
+
55
+ atom_elements :author, :authors, Atom::Author
56
+ atom_elements :contributor, :contributors, Atom::Contributor
57
+
58
+ atom_string :generator # XXX with uri and version attributes!
59
+ atom_string :icon
60
+ atom_string :logo
61
+
62
+ atom_element :rights, Atom::Rights
63
+
64
+ atom_elements :entry, :entries, Atom::Entry
65
+
66
+ include Enumerable
67
+
68
+ def inspect # :nodoc:
69
+ "<#{@uri} entries: #{entries.length} title='#{title}'>"
70
+ end
71
+
72
+ # Create a new Feed that can be found at feed_uri and retrieved
73
+ # using an Atom::HTTP object http
74
+ def initialize feed_uri = nil, http = Atom::HTTP.new
75
+ @entries = []
76
+ @http = http
77
+
78
+ if feed_uri
79
+ @uri = feed_uri.to_uri
80
+ self.base = feed_uri
81
+ end
82
+
83
+ super()
84
+ end
85
+
86
+ # iterates over a feed's entries
87
+ def each &block
88
+ @entries.each &block
89
+ end
90
+
91
+ def empty?
92
+ @entries.empty?
93
+ end
94
+
95
+ # gets everything in the logical feed (could be a lot of stuff)
96
+ # (see RFC 5005)
97
+ def get_everything!
98
+ self.update!
99
+
100
+ prev = @prev
101
+ while prev
102
+ prev.update!
103
+
104
+ self.merge_entries! prev
105
+ prev = prev.prev
106
+ end
107
+
108
+ nxt = @next
109
+ while nxt
110
+ nxt.update!
111
+
112
+ self.merge_entries! nxt
113
+ nxt = nxt.next
114
+ end
115
+
116
+ self
117
+ end
118
+
119
+ # merges the entries from another feed into this one
120
+ def merge_entries! other_feed
121
+ other_feed.each do |entry|
122
+ # TODO: add atom:source elements
123
+ self << entry
124
+ end
125
+ end
126
+
127
+ # like #merge, but in place
128
+ def merge! other_feed
129
+ [:id, :title, :subtitle, :updated, :rights, :logo, :icon].each do |p|
130
+ if (v = other_feed.get(p))
131
+ set p, v
132
+ end
133
+ end
134
+
135
+ [:links, :categories, :authors, :contributors].each do |p|
136
+ other_feed.get(p).each do |e|
137
+ get(p) << e
138
+ end
139
+ end
140
+
141
+ @extensions = other_feed.extensions
142
+
143
+ merge_entries! other_feed
144
+ end
145
+
146
+ # merges "important" properties of this feed with another one,
147
+ # returning a new feed
148
+ def merge other_feed
149
+ feed = self.clone
150
+
151
+ feed.merge! other_feed
152
+
153
+ feed
154
+ end
155
+
156
+ # fetches this feed's URL, parses the result and #merge!s
157
+ # changes, new entries, &c.
158
+ #
159
+ # (note that this is different from Atom::Entry#updated!
160
+ def update!
161
+ raise(RuntimeError, "can't fetch without a uri.") unless @uri
162
+
163
+ res = @http.get(@uri, "Accept" => "application/atom+xml")
164
+
165
+ if @etag and res['etag'] == @etag
166
+ # we're already all up to date
167
+ return self
168
+ elsif res.code == "410"
169
+ raise Atom::FeedGone, "410 Gone (#{@uri})"
170
+ elsif res.code != "200"
171
+ raise Atom::HTTPException, "Unexpected HTTP response code: #{res.code}"
172
+ end
173
+
174
+ # we'll be forgiving about feed content types.
175
+ res.validate_content_type(["application/atom+xml",
176
+ "application/xml",
177
+ "text/xml"])
178
+
179
+ @etag = res["ETag"] if res["ETag"]
180
+
181
+ xml = res.body
182
+
183
+ coll = REXML::Document.new(xml)
184
+
185
+ update_el = REXML::XPath.first(coll, "/atom:feed/atom:updated", { "atom" => Atom::NS } )
186
+
187
+ # the feed hasn't been updated, don't do anything.
188
+ if update_el and self.updated and self.updated >= Time.parse(update_el.text)
189
+ return self
190
+ end
191
+
192
+ coll = self.class.parse(coll.root, self.base.to_s)
193
+ merge! coll
194
+
195
+ if abs_uri = next_link
196
+ @next = self.class.new(abs_uri.to_s, @http)
197
+ end
198
+
199
+ if abs_uri = previous_link
200
+ @prev = self.class.new(abs_uri.to_s, @http)
201
+ end
202
+
203
+ self
204
+ end
205
+
206
+ atom_link :previous_link, :rel => 'previous'
207
+ atom_link :next_link, :rel => 'next'
208
+
209
+ # adds an entry to this feed. if this feed already contains an
210
+ # entry with the same id, the newest one is used.
211
+ def << entry
212
+ existing = entries.find do |e|
213
+ e.id == entry.id
214
+ end
215
+
216
+ if not existing
217
+ @entries << entry
218
+ elsif not existing.updated or (existing.updated and entry.updated and entry.updated >= existing.updated)
219
+ @entries[@entries.index(existing)] = entry
220
+ end
221
+ end
222
+ end
223
+ end
data/lib/atom/http.rb ADDED
@@ -0,0 +1,417 @@
1
+ require "net/http"
2
+ require "net/https"
3
+ require "uri"
4
+
5
+ require "atom/cache"
6
+
7
+ require "sha1"
8
+ require "digest/md5"
9
+
10
+ module URI # :nodoc: all
11
+ class Generic; def to_uri; self; end; end
12
+ end
13
+
14
+ class String # :nodoc:
15
+ def to_uri; URI.parse(self); end
16
+ end
17
+
18
+ module Atom
19
+ UA = "atom-tools 2.0.1"
20
+
21
+ module DigestAuth
22
+ CNONCE = Digest::MD5.hexdigest("%x" % (Time.now.to_i + rand(65535)))
23
+
24
+ @@nonce_count = -1
25
+
26
+ # quoted-strings plus a few special cases for Digest
27
+ def parse_wwwauth_digest param_string
28
+ params = parse_quoted_wwwauth param_string
29
+ qop = params[:qop] ? params[:qop].split(",") : nil
30
+
31
+ param_string.gsub(/stale=([^,]*)/) do
32
+ params[:stale] = ($1.downcase == "true")
33
+ end
34
+
35
+ params[:algorithm] = "MD5"
36
+ param_string.gsub(/algorithm=([^,]*)/) { params[:algorithm] = $1 }
37
+
38
+ params
39
+ end
40
+
41
+ def h(data); Digest::MD5.hexdigest(data); end
42
+ def kd(secret, data); h(secret + ":" + data); end
43
+
44
+ # HTTP Digest authentication (RFC 2617)
45
+ def digest_authenticate(req, url, param_string = "")
46
+ raise "Digest authentication requires a WWW-Authenticate header" if param_string.empty?
47
+
48
+ params = parse_wwwauth_digest(param_string)
49
+ qop = params[:qop]
50
+
51
+ user, pass = username_and_password_for_realm(url, params[:realm])
52
+
53
+ if params[:algorithm] == "MD5"
54
+ a1 = user + ":" + params[:realm] + ":" + pass
55
+ else
56
+ # XXX MD5-sess
57
+ raise "I only support MD5 digest authentication (not #{params[:algorithm].inspect})"
58
+ end
59
+
60
+ if qop.nil? or qop.member? "auth"
61
+ a2 = req.method + ":" + req.path
62
+ else
63
+ # XXX auth-int
64
+ raise "only 'auth' qop supported (none of: #{qop.inspect})"
65
+ end
66
+
67
+ if qop.nil?
68
+ response = kd(h(a1), params[:nonce] + ":" + h(a2))
69
+ else
70
+ @@nonce_count += 1
71
+ nc = ('%08x' % @@nonce_count)
72
+
73
+ # XXX auth-int
74
+ data = "#{params[:nonce]}:#{nc}:#{CNONCE}:#{"auth"}:#{h(a2)}"
75
+
76
+ response = kd(h(a1), data)
77
+ end
78
+
79
+ header = %Q<Digest username="#{user}", uri="#{req.path}", realm="#{params[:realm]}", response="#{response}", nonce="#{params[:nonce]}">
80
+
81
+ if params[:opaque]
82
+ header += %Q<, opaque="#{params[:opaque]}">
83
+ end
84
+
85
+ if params[:algorithm] != "MD5"
86
+ header += ", algorithm=#{algo}"
87
+ end
88
+
89
+ if qop
90
+ # XXX auth-int
91
+ header += %Q<, nc=#{nc}, cnonce="#{CNONCE}", qop=auth>
92
+ end
93
+
94
+ req["Authorization"] = header
95
+ end
96
+ end
97
+
98
+ class HTTPException < RuntimeError # :nodoc:
99
+ end
100
+ class Unauthorized < Atom::HTTPException # :nodoc:
101
+ end
102
+ class WrongMimetype < Atom::HTTPException # :nodoc:
103
+ end
104
+
105
+ # An object which handles the details of HTTP - particularly
106
+ # authentication and caching (neither of which are fully implemented).
107
+ #
108
+ # This object can be used on its own, or passed to an Atom::Service,
109
+ # Atom::Collection or Atom::Feed, where it will be used for requests.
110
+ #
111
+ # All its HTTP methods return a Net::HTTPResponse
112
+ class HTTP
113
+ include DigestAuth
114
+
115
+ # used by the default #when_auth
116
+ attr_accessor :user, :pass
117
+
118
+ # the token used for Google's AuthSub authentication
119
+ attr_accessor :token
120
+
121
+ # when set to :basic, :wsse or :authsub, this will send an
122
+ # Authentication header with every request instead of waiting for a
123
+ # challenge from the server.
124
+ #
125
+ # be careful; always_auth :basic will send your username and
126
+ # password in plain text to every URL this object requests.
127
+ #
128
+ # :digest won't work, since Digest authentication requires an
129
+ # initial challenge to generate a response
130
+ #
131
+ # defaults to nil
132
+ attr_accessor :always_auth
133
+
134
+ # automatically handle redirects, even for POST/PUT/DELETE requests?
135
+ #
136
+ # defaults to false, which will transparently redirect GET requests
137
+ # but return a Net::HTTPRedirection object when the server
138
+ # indicates to redirect a POST/PUT/DELETE
139
+ attr_accessor :allow_all_redirects
140
+
141
+ # if set, 'cache' should be a directory for a disk cache, or an object
142
+ # with the same interface as Atom::FileCache
143
+ def initialize cache = nil
144
+ if cache.is_a? String
145
+ @cache = FileCache.new(cache)
146
+ elsif cache
147
+ @cache = cache
148
+ else
149
+ @cache = NilCache.new
150
+ end
151
+
152
+ # initialize default #when_auth
153
+ @get_auth_details = lambda do |abs_url, realm|
154
+ if @user and @pass
155
+ [@user, @pass]
156
+ else
157
+ nil
158
+ end
159
+ end
160
+ end
161
+
162
+ # GETs an url
163
+ def get url, headers = {}
164
+ http_request(url, Net::HTTP::Get, nil, headers)
165
+ end
166
+
167
+ # POSTs body to an url
168
+ def post url, body, headers = {}
169
+ http_request(url, Net::HTTP::Post, body, headers)
170
+ end
171
+
172
+ # PUTs body to an url
173
+ def put url, body, headers = {}
174
+ http_request(url, Net::HTTP::Put, body, headers)
175
+ end
176
+
177
+ # DELETEs to url
178
+ def delete url, body = nil, headers = {}
179
+ http_request(url, Net::HTTP::Delete, body, headers)
180
+ end
181
+
182
+ # a block that will be called when a remote server responds with
183
+ # 401 Unauthorized, so that your application can prompt for
184
+ # authentication details.
185
+ #
186
+ # the default is to use the values of @user and @pass.
187
+ #
188
+ # your block will be called with two parameters:
189
+ # abs_url:: the base URL of the request URL
190
+ # realm:: the realm used in the WWW-Authenticate header (maybe nil)
191
+ #
192
+ # your block should return [username, password], or nil
193
+ def when_auth &block # :yields: abs_url, realm
194
+ @get_auth_details = block
195
+ end
196
+
197
+ # GET a URL and turn it into an Atom::Entry
198
+ def get_atom_entry(url)
199
+ res = get(url, "Accept" => "application/atom+xml")
200
+
201
+ # XXX handle other HTTP codes
202
+ if res.code != "200"
203
+ raise Atom::HTTPException, "failed to fetch entry: expected 200 OK, got #{res.code}"
204
+ end
205
+
206
+ # be picky for atom:entrys
207
+ res.validate_content_type( [ "application/atom+xml" ] )
208
+
209
+ Atom::Entry.parse(res.body, url)
210
+ end
211
+
212
+ # PUT an Atom::Entry to a URL
213
+ def put_atom_entry(entry, url = entry.edit_url)
214
+ raise "Cowardly refusing to PUT a non-Atom::Entry (#{entry.class})" unless entry.is_a? Atom::Entry
215
+ headers = {"Content-Type" => "application/atom+xml" }
216
+
217
+ put(url, entry.to_s, headers)
218
+ end
219
+
220
+ private
221
+ # parses plain quoted-strings
222
+ def parse_quoted_wwwauth param_string
223
+ params = {}
224
+
225
+ param_string.gsub(/(\w+)="(.*?)"/) { params[$1.to_sym] = $2 }
226
+
227
+ params
228
+ end
229
+
230
+ # HTTP Basic authentication (RFC 2617)
231
+ def basic_authenticate(req, url, param_string = "")
232
+ params = parse_quoted_wwwauth(param_string)
233
+
234
+ user, pass = username_and_password_for_realm(url, params[:realm])
235
+
236
+ req.basic_auth user, pass
237
+ end
238
+
239
+ # is this the right way to do it? who knows, there's no
240
+ # spec!
241
+ # <http://necronomicorp.com/lab/atom-authentication-sucks>
242
+ #
243
+ # thanks to H. Miyamoto for clearing things up.
244
+ def wsse_authenticate(req, url, params = {})
245
+ user, pass = username_and_password_for_realm(url, params["realm"])
246
+
247
+ nonce = rand(16**32).to_s(16)
248
+ nonce_enc = [nonce].pack('m').chomp
249
+ now = Time.now.gmtime.iso8601
250
+
251
+ digest = [Digest::SHA1.digest(nonce + now + pass)].pack("m").chomp
252
+
253
+ req['X-WSSE'] = %Q<UsernameToken Username="#{user}", PasswordDigest="#{digest}", Nonce="#{nonce_enc}", Created="#{now}">
254
+ req["Authorization"] = 'WSSE profile="UsernameToken"'
255
+ end
256
+
257
+ def authsub_authenticate req, url
258
+ req["Authorization"] = %{AuthSub token="#{@token}"}
259
+ end
260
+
261
+ def username_and_password_for_realm(url, realm)
262
+ abs_url = (url + "/").to_s
263
+ user, pass = @get_auth_details.call(abs_url, realm)
264
+
265
+ unless user and pass
266
+ raise Unauthorized, "You must provide a username and password"
267
+ end
268
+
269
+ [ user, pass ]
270
+ end
271
+
272
+ # performs a generic HTTP request.
273
+ def http_request(url_s, method, body = nil, headers = {}, www_authenticate = nil, redirect_limit = 5)
274
+ cachekey = url_s.to_s
275
+
276
+ cached_value = @cache[cachekey]
277
+ if cached_value
278
+ sock = Net::BufferedIO.new(StringIO.new(cached_value))
279
+ info = Net::HTTPResponse.read_new(sock)
280
+ info.reading_body(sock, true) {}
281
+
282
+ if method == Net::HTTP::Put and info.key? 'etag' and not headers['If-Match']
283
+ headers['If-Match'] = info['etag']
284
+ end
285
+ end
286
+
287
+ if cached_value and not [Net::HTTP::Get, Net::HTTP::Head].member? method
288
+ @cache.delete(cachekey)
289
+ elsif cached_value
290
+ entry_disposition = _entry_disposition(info, headers)
291
+
292
+ if entry_disposition == :FRESH
293
+ info.extend Atom::HTTPResponse
294
+
295
+ return info
296
+ elsif entry_disposition == :STALE
297
+ if info.key? 'etag' and not headers['If-None-Match']
298
+ headers['If-None-Match'] = info['etag']
299
+ end
300
+ if info.key? 'last-modified' and not headers['Last-Modified']
301
+ headers['If-Modified-Since'] = info['last-modified']
302
+ end
303
+ end
304
+ end
305
+
306
+ req, url = new_request(url_s, method, headers)
307
+
308
+ # two reasons to authenticate;
309
+ if @always_auth
310
+ self.send("#{@always_auth}_authenticate", req, url)
311
+ elsif www_authenticate
312
+ dispatch_authorization www_authenticate, req, url
313
+ end
314
+
315
+ http_obj = Net::HTTP.new(url.host, url.port)
316
+ http_obj.use_ssl = true if url.scheme == "https"
317
+
318
+ res = http_obj.start do |h|
319
+ h.request(req, body)
320
+ end
321
+
322
+ # a bit of added convenience
323
+ res.extend Atom::HTTPResponse
324
+
325
+ case res
326
+ when Net::HTTPUnauthorized
327
+ if @always_auth or www_authenticate or not res["WWW-Authenticate"] # XXX and not stale (Digest only)
328
+ # we've tried the credentials you gave us once
329
+ # and failed, or the server gave us no way to fix it
330
+ raise Unauthorized, "Your authorization was rejected"
331
+ else
332
+ # once more, with authentication
333
+ res = http_request(url_s, method, body, headers, res["WWW-Authenticate"])
334
+
335
+ if res.kind_of? Net::HTTPUnauthorized
336
+ raise Unauthorized, "Your authorization was rejected"
337
+ end
338
+ end
339
+ when Net::HTTPRedirection
340
+ if res.code == "304" and method == Net::HTTP::Get
341
+ res.end2end_headers.each { |k| info[k] = res[k] }
342
+
343
+ res = info
344
+
345
+ res["Content-Length"] = res.body.length
346
+
347
+ res.extend Atom::HTTPResponse
348
+
349
+ _updateCache(headers, res, @cache, cachekey)
350
+ elsif res["Location"] and (allow_all_redirects or [Net::HTTP::Get, Net::HTTP::Head].member? method)
351
+ raise HTTPException, "Too many redirects" if redirect_limit.zero?
352
+
353
+ res = http_request res["Location"], method, body, headers, nil, (redirect_limit - 1)
354
+ end
355
+ when Net::HTTPOK, Net::HTTPNonAuthoritativeInformation
356
+ unless res.key? 'Content-Location'
357
+ res['Content-Location'] = url_s
358
+ end
359
+ _updateCache(headers, res, @cache, cachekey)
360
+ end
361
+
362
+ res
363
+ end
364
+
365
+ def new_request(url_string, method, init_headers = {})
366
+ headers = { "User-Agent" => UA }.merge(init_headers)
367
+
368
+ url = url_string.to_uri
369
+
370
+ rel = url.path
371
+ rel += "?" + url.query if url.query
372
+
373
+ [method.new(rel, headers), url]
374
+ end
375
+
376
+ def dispatch_authorization www_authenticate, req, url
377
+ param_string = www_authenticate.sub(/^(\w+) /, "")
378
+ auth_method = ($~[1].downcase + "_authenticate").to_sym
379
+
380
+ if self.respond_to? auth_method, true # includes private methods
381
+ self.send(auth_method, req, url, param_string)
382
+ else
383
+ # didn't support the first offered, find the next header
384
+ next_to_try = www_authenticate.sub(/.* ([\w]+ )/, '\1')
385
+ if next_to_try == www_authenticate
386
+ # this was the last WWW-Authenticate header
387
+ raise Atom::Unauthorized, "No support for offered authentication types"
388
+ else
389
+ dispatch_authorization next_to_try, req, url
390
+ end
391
+ end
392
+ end
393
+ end
394
+
395
+ module HTTPResponse
396
+ HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', 'upgrade']
397
+
398
+ # this should probably support ranges (eg. text/*)
399
+ def validate_content_type( valid )
400
+ raise Atom::HTTPException, "HTTP response contains no Content-Type!" if not self.content_type or self.content_type.empty?
401
+
402
+ media_type = self.content_type.split(";").first
403
+
404
+ unless valid.member? media_type.downcase
405
+ raise Atom::WrongMimetype, "unexpected response Content-Type: #{media_type.inspect}. should be one of: #{valid.inspect}"
406
+ end
407
+ end
408
+
409
+ def end2end_headers
410
+ hopbyhop = HOP_BY_HOP
411
+ if self['connection']
412
+ hopbyhop += self['connection'].split(',').map { |x| x.strip }
413
+ end
414
+ @header.keys.reject { |x| hopbyhop.member? x.downcase }
415
+ end
416
+ end
417
+ end