feedtools 0.2.10 → 0.2.11

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.
@@ -0,0 +1,2073 @@
1
+ module FeedTools
2
+ # The <tt>FeedTools::Feed</tt> class represents a web feed's structure.
3
+ class Feed
4
+ include REXML # :nodoc:
5
+
6
+ # Represents a feed/feed item's category
7
+ class Category
8
+
9
+ # The category term value
10
+ attr_accessor :term
11
+ # The categorization scheme
12
+ attr_accessor :scheme
13
+ # A human-readable description of the category
14
+ attr_accessor :label
15
+
16
+ alias_method :value, :term
17
+ alias_method :category, :term
18
+ alias_method :domain, :scheme
19
+ end
20
+
21
+ # Represents a feed/feed item's author
22
+ class Author
23
+
24
+ # The author's real name
25
+ attr_accessor :name
26
+ # The author's email address
27
+ attr_accessor :email
28
+ # The url of the author's homepage
29
+ attr_accessor :url
30
+ # The raw value of the author tag if present
31
+ attr_accessor :raw
32
+ end
33
+
34
+ # Represents a feed's image
35
+ class Image
36
+
37
+ # The image's title
38
+ attr_accessor :title
39
+ # The image's description
40
+ attr_accessor :description
41
+ # The image's url
42
+ attr_accessor :url
43
+ # The url to link the image to
44
+ attr_accessor :link
45
+ # The width of the image
46
+ attr_accessor :width
47
+ # The height of the image
48
+ attr_accessor :height
49
+ # The style of the image
50
+ # Possible values are "icon", "image", or "image-wide"
51
+ attr_accessor :style
52
+ end
53
+
54
+ # Represents a feed's text input element.
55
+ # Be aware that this will be ignored for feed generation. It's a
56
+ # pointless element that aggregators usually ignore and it doesn't have an
57
+ # equivalent in all feeds types.
58
+ class TextInput
59
+
60
+ # The label of the Submit button in the text input area.
61
+ attr_accessor :title
62
+ # The description explains the text input area.
63
+ attr_accessor :description
64
+ # The URL of the CGI script that processes text input requests.
65
+ attr_accessor :link
66
+ # The name of the text object in the text input area.
67
+ attr_accessor :name
68
+ end
69
+
70
+ # Represents a feed's cloud.
71
+ # Be aware that this will be ignored for feed generation.
72
+ class Cloud
73
+
74
+ # The domain of the cloud.
75
+ attr_accessor :domain
76
+ # The path for the cloud.
77
+ attr_accessor :path
78
+ # The port the cloud is listening on.
79
+ attr_accessor :port
80
+ # The web services protocol the cloud uses.
81
+ # Possible values are either "xml-rpc" or "soap".
82
+ attr_accessor :protocol
83
+ # The procedure to use to request notification.
84
+ attr_accessor :register_procedure
85
+ end
86
+
87
+ # Represents a simple hyperlink
88
+ class Link
89
+
90
+ # The url that is being linked to
91
+ attr_accessor :url
92
+ # The content of the hyperlink
93
+ attr_accessor :value
94
+
95
+ alias_method :href, :url
96
+ end
97
+
98
+ # Initialize the feed object
99
+ def initialize
100
+ super
101
+ @cache_object = nil
102
+ @http_headers = nil
103
+ @xml_doc = nil
104
+ @xml_data = nil
105
+ @root_node = nil
106
+ @channel_node = nil
107
+ @url = nil
108
+ @id = nil
109
+ @title = nil
110
+ @description = nil
111
+ @link = nil
112
+ @time_to_live = nil
113
+ @items = nil
114
+ @live = false
115
+ end
116
+
117
+ # Raises an exception if an invalid option has been specified to
118
+ # prevent misspellings from slipping through
119
+ def Feed.validate_options(valid_option_keys, supplied_option_keys)
120
+ unknown_option_keys = supplied_option_keys - valid_option_keys
121
+ unless unknown_option_keys.empty?
122
+ raise ArgumentError, "Unknown options: #{unknown_option_keys}"
123
+ end
124
+ end
125
+ class << self; private :validate_options; end
126
+
127
+ # Loads the feed specified by the url, pulling the data from the
128
+ # cache if it hasn't expired.
129
+ # Options are:
130
+ # * <tt>:cache_only</tt> - If set to true, the feed will only be
131
+ # pulled from the cache.
132
+ def Feed.open(url, options={})
133
+ validate_options([ :cache_only ],
134
+ options.keys)
135
+ options = { :cache_only => false }.merge(options)
136
+
137
+ # clean up the url
138
+ url = FeedTools.normalize_url(url)
139
+
140
+ # create and load the new feed
141
+ feed = Feed.new
142
+ feed.url = url
143
+ feed.update! unless options[:cache_only]
144
+ return feed
145
+ end
146
+
147
+ # Loads the feed from the remote url if the feed has expired from the cache or cannot be
148
+ # retrieved from the cache for some reason.
149
+ def update!
150
+ if self.http_headers.nil? && !(self.cache_object.nil?) &&
151
+ !(self.cache_object.http_headers.nil?)
152
+ @http_headers = YAML.load(self.cache_object.http_headers)
153
+ @http_headers = {} unless @http_headers.kind_of? Hash
154
+ end
155
+ if self.expired? == false
156
+ @live = false
157
+ else
158
+ load_remote_feed!
159
+ end
160
+ end
161
+
162
+ # Attempts to load the feed from the remote location. Requires the url
163
+ # field to be set. If an etag or the last_modified date has been set,
164
+ # attempts to use them to prevent unnecessary reloading of identical
165
+ # content.
166
+ def load_remote_feed!
167
+ @live = true
168
+ if self.http_headers.nil? && !(self.cache_object.nil?) &&
169
+ !(self.cache_object.http_headers.nil?)
170
+ @http_headers = YAML.load(self.cache_object.http_headers)
171
+ end
172
+
173
+ if (self.url =~ /^feed:/) == 0
174
+ # Woah, Nelly, how'd that happen? You should've already been
175
+ # corrected. So let's fix that url. And please,
176
+ # just use less crappy browsers instead of badly defined
177
+ # pseudo-protocol hacks.
178
+ self.url = FeedTools.normalize_url(self.url)
179
+ end
180
+
181
+ # Find out what method we're going to be using to obtain this feed.
182
+ uri = URI.parse(self.url)
183
+ retrieval_method = "http"
184
+ case uri.scheme
185
+ when "http"
186
+ retrieval_method = "http"
187
+ when "ftp"
188
+ retrieval_method = "ftp"
189
+ when "file"
190
+ retrieval_method = "file"
191
+ when nil
192
+ raise FeedAccessError,
193
+ "No protocol was specified in the url."
194
+ else
195
+ raise FeedAccessError,
196
+ "Cannot retrieve feed using unrecognized protocol: " + uri.scheme
197
+ end
198
+
199
+ # No need for http headers unless we're actually doing http
200
+ if retrieval_method == "http"
201
+ # Set up the appropriate http headers
202
+ headers = {}
203
+ unless self.http_headers.nil?
204
+ headers["If-None-Match"] =
205
+ self.http_headers['etag'] unless self.http_headers['etag'].nil?
206
+ headers["If-Modified-Since"] =
207
+ self.http_headers['last-modified'] unless
208
+ self.http_headers['last-modified'].nil?
209
+ end
210
+ headers["User-Agent"] =
211
+ FeedTools.user_agent unless FeedTools.user_agent.nil?
212
+
213
+ # The http feed access method
214
+ http_fetch = lambda do |feed_url, http_headers, redirect_limit,
215
+ response_chain, no_headers|
216
+ raise FeedAccessError, 'Redirect too deep' if redirect_limit == 0
217
+ feed_uri = nil
218
+ begin
219
+ feed_uri = URI.parse(feed_url)
220
+ rescue URI::InvalidURIError
221
+ # Uh, maybe try to fix it?
222
+ feed_uri = URI.parse(FeedTools.normalize_url(feed_url))
223
+ end
224
+
225
+ # Borrowed from open-uri:
226
+ # According to RFC2616 14.23, Host: request-header field should be
227
+ # set to an origin server.
228
+ # But net/http wrongly set a proxy server if an absolute URI is
229
+ # specified as a request URI.
230
+ # So override it here explicitly.
231
+ http_headers['Host'] = feed_uri.host
232
+ http_headers['Host'] += ":#{feed_uri.port}" if feed_uri.port
233
+
234
+ Net::HTTP.start(feed_uri.host, (feed_uri.port or 80)) do |http|
235
+ final_uri = feed_uri.path
236
+ final_uri += ('?' + feed_uri.query) if feed_uri.query
237
+ http_headers = {} if no_headers
238
+ response = http.request_get(final_uri, http_headers)
239
+
240
+ case response
241
+ when Net::HTTPSuccess
242
+ # We've reached the final destination, process all previous
243
+ # redirections, and see if we need to update the url.
244
+ for redirected_response in response_chain
245
+ if redirected_response.last.code.to_i == 301
246
+ # Reset the cache object or we may get duplicate entries
247
+ self.cache_object = nil
248
+ self.url = redirected_response.last['location']
249
+ else
250
+ # Jump out as soon as we hit anything that isn't a
251
+ # permanently moved redirection.
252
+ break
253
+ end
254
+ end
255
+ response
256
+ when Net::HTTPRedirection
257
+ if response.code.to_i == 304
258
+ response.error!
259
+ else
260
+ if response['location'].nil?
261
+ raise FeedAccessError,
262
+ "No location to redirect to supplied: " + response.code
263
+ end
264
+ response_chain << [feed_url, response]
265
+ new_location = response['location']
266
+ if response_chain.assoc(new_location) != nil
267
+ raise FeedAccessError, "Redirection loop detected."
268
+ end
269
+
270
+ # Find out if we've already seen the url we've been
271
+ # redirected to.
272
+ cached_feed = FeedTools::Feed.open(new_location,
273
+ :cache_only => true)
274
+ found_redirect = false
275
+ if cached_feed.cache_object.new_record? != true
276
+ unless cached_feed.expired?
277
+ self.url = cached_feed.url
278
+ @live = false
279
+ found_redirect = true
280
+ end
281
+ end
282
+ unless found_redirect
283
+ # TODO: deal with stupid people using relative urls
284
+ # in Location header
285
+ # =================================================
286
+ http_fetch.call(new_location, http_headers,
287
+ redirect_limit - 1, response_chain, no_headers)
288
+ else
289
+ response
290
+ end
291
+ end
292
+ else
293
+ class << response
294
+ def response_chain
295
+ return @response_chain
296
+ end
297
+ end
298
+ response.instance_variable_set("@response_chain",
299
+ response_chain)
300
+ response.error!
301
+ end
302
+ end
303
+ end
304
+
305
+ begin
306
+ begin
307
+ @http_response = http_fetch.call(self.url, headers, 10, [], false)
308
+ rescue => error
309
+ if error.respond_to?(:response)
310
+ # You might not believe this, but...
311
+ #
312
+ # Under certain circumstances, web servers will try to block
313
+ # based on the User-Agent header. This is *retarded*. But
314
+ # we won't let their stupid error stop us!
315
+ #
316
+ # This is, of course, a quick-n-dirty hack. But at least
317
+ # we get to blame other people's bad software and/or bad
318
+ # configuration files.
319
+ if error.response.code.to_i == 404 &&
320
+ FeedTools.user_agent != nil
321
+ @http_response = http_fetch.call(self.url, {}, 10, [], true)
322
+ if @http_response != nil && @http_response.code.to_i == 200
323
+ warn("The server appears to be blocking based on the " +
324
+ "User-Agent header. This is stupid, and you should " +
325
+ "inform the webmaster of this.")
326
+ end
327
+ else
328
+ raise error
329
+ end
330
+ else
331
+ raise error
332
+ end
333
+ end
334
+ unless @http_response.kind_of? Net::HTTPRedirection
335
+ @http_headers = {}
336
+ self.http_response.each_header do |header|
337
+ self.http_headers[header.first.downcase] = header.last
338
+ end
339
+ self.last_retrieved = Time.now
340
+ self.xml_data = self.http_response.body
341
+ end
342
+ rescue FeedAccessError
343
+ @live = false
344
+ if self.xml_data.nil?
345
+ raise
346
+ end
347
+ rescue Timeout::Error
348
+ # if we time out, do nothing, it should fall back to the xml_data
349
+ # stored in the cache.
350
+ @live = false
351
+ if self.xml_data.nil?
352
+ raise
353
+ end
354
+ rescue Errno::ECONNRESET
355
+ # if the connection gets reset by peer, oh well, fall back to the
356
+ # xml_data stored in the cache
357
+ @live = false
358
+ if self.xml_data.nil?
359
+ raise
360
+ end
361
+ rescue => error
362
+ # heck, if anything at all bad happens, fall back to the xml_data
363
+ # stored in the cache.
364
+
365
+ # If we can, get the HTTPResponse...
366
+ @http_response = nil
367
+ if error.respond_to?(:each_header)
368
+ @http_response = error
369
+ end
370
+ if error.respond_to?(:response) &&
371
+ error.response.respond_to?(:each_header)
372
+ @http_response = error.response
373
+ end
374
+ if @http_response != nil
375
+ @http_headers = {}
376
+ self.http_response.each_header do |header|
377
+ self.http_headers[header.first] = header.last
378
+ end
379
+ if self.http_response.code.to_i == 304
380
+ self.last_retrieved = Time.now
381
+ end
382
+ end
383
+ @live = false
384
+ if self.xml_data.nil?
385
+ if error.respond_to?(:response) &&
386
+ error.response.respond_to?(:response_chain)
387
+ redirects = error.response.response_chain.map do |pair|
388
+ pair.first
389
+ end
390
+ error.message << (" - Redirects: " + redirects.inspect)
391
+ end
392
+ raise error
393
+ end
394
+ end
395
+ elsif retrieval_method == "https"
396
+ # Not supported... yet
397
+ elsif retrieval_method == "ftp"
398
+ # Not supported... yet
399
+ # Technically, CDF feeds are supposed to be able to be accessed directly
400
+ # from an ftp server. This is silly, but we'll humor Microsoft.
401
+ #
402
+ # Eventually.
403
+ elsif retrieval_method == "file"
404
+ # Now that we've gone to all that trouble to ensure the url begins
405
+ # with 'file://', strip the 'file://' off the front of the url.
406
+ file_name = self.url.gsub(/^file:\/\//, "")
407
+ begin
408
+ open(file_name) do |file|
409
+ @http_response = nil
410
+ @http_headers = {}
411
+ self.last_retrieved = Time.now
412
+ self.xml_data = file.read
413
+ end
414
+ rescue
415
+ @live = false
416
+ # In this case, pulling from the cache is probably not going
417
+ # to help at all, and the use should probably be immediately
418
+ # appraised of the problem. Raise the exception.
419
+ raise
420
+ end
421
+ end
422
+ unless self.cache_object.nil?
423
+ begin
424
+ self.save
425
+ rescue
426
+ end
427
+ end
428
+ end
429
+
430
+ # Returns the relevant information from an http request.
431
+ def http_response
432
+ return @http_response
433
+ end
434
+
435
+ # Returns a hash of the http headers from the response.
436
+ def http_headers
437
+ return @http_headers
438
+ end
439
+
440
+ # Returns the feed's raw xml data.
441
+ def xml_data
442
+ if @xml_data.nil?
443
+ unless self.cache_object.nil?
444
+ @xml_data = self.cache_object.xml_data
445
+ end
446
+ end
447
+ return @xml_data
448
+ end
449
+
450
+ # Sets the feed's xml data.
451
+ def xml_data=(new_xml_data)
452
+ @xml_data = new_xml_data
453
+ unless self.cache_object.nil?
454
+ self.cache_object.xml_data = new_xml_data
455
+ end
456
+ end
457
+
458
+ # Returns a REXML Document of the xml_data
459
+ def xml
460
+ if @xml_doc.nil?
461
+ begin
462
+ # TODO: :ignore_whitespace_nodes => :all
463
+ # Add that?
464
+ # ======================================
465
+ @xml_doc = Document.new(xml_data)
466
+ rescue
467
+ # Something failed, attempt to repair the xml with htree.
468
+ @xml_doc = HTree.parse(xml_data).to_rexml
469
+ end
470
+ end
471
+ return @xml_doc
472
+ end
473
+
474
+ # Returns the first node within the channel_node that matches the xpath query.
475
+ def find_node(xpath)
476
+ return XPath.first(channel_node, xpath)
477
+ end
478
+
479
+ # Returns all nodes within the channel_node that match the xpath query.
480
+ def find_all_nodes(xpath)
481
+ return XPath.match(channel_node, xpath)
482
+ end
483
+
484
+ # Returns the root node of the feed.
485
+ def root_node
486
+ if @root_node.nil?
487
+ # TODO: Fix this so that added content at the end of the file doesn't
488
+ # break this stuff.
489
+ # E.g.: http://smogzer.tripod.com/smog.rdf
490
+ # ===================================================================
491
+ @root_node = xml.root
492
+ end
493
+ return @root_node
494
+ end
495
+
496
+ # Returns the channel node of the feed.
497
+ def channel_node
498
+ if @channel_node.nil? && root_node != nil
499
+ @channel_node = XPath.first(root_node, "channel")
500
+ if @channel_node == nil
501
+ @channel_node = XPath.first(root_node, "CHANNEL")
502
+ end
503
+ if @channel_node == nil
504
+ @channel_node = XPath.first(root_node, "feedinfo")
505
+ end
506
+ if @channel_node == nil
507
+ @channel_node = root_node
508
+ end
509
+ end
510
+ return @channel_node
511
+ end
512
+
513
+ # The cache object that handles the feed persistence.
514
+ def cache_object
515
+ unless FeedTools.feed_cache.nil?
516
+ if @cache_object.nil?
517
+ begin
518
+ if @id != nil
519
+ @cache_object = FeedTools.feed_cache.find_by_id(@id)
520
+ elsif @url != nil
521
+ @cache_object = FeedTools.feed_cache.find_by_url(@url)
522
+ end
523
+ if @cache_object.nil?
524
+ @cache_object = FeedTools.feed_cache.new
525
+ end
526
+ rescue
527
+ end
528
+ end
529
+ end
530
+ return @cache_object
531
+ end
532
+
533
+ # Sets the cache object for this feed.
534
+ #
535
+ # This can be any object, but it must accept the following messages:
536
+ # url
537
+ # url=
538
+ # title
539
+ # title=
540
+ # link
541
+ # link=
542
+ # xml_data
543
+ # xml_data=
544
+ # etag
545
+ # etag=
546
+ # last_modified
547
+ # last_modified=
548
+ # save
549
+ def cache_object=(new_cache_object)
550
+ @cache_object = new_cache_object
551
+ end
552
+
553
+ # Returns the type of feed
554
+ # Possible values:
555
+ # "rss", "atom", "cdf", "!okay/news"
556
+ def feed_type
557
+ if @feed_type.nil?
558
+ case self.root_node.name.downcase
559
+ when "feed"
560
+ @feed_type = "atom"
561
+ when "rdf:rdf"
562
+ @feed_type = "rss"
563
+ when "rdf"
564
+ @feed_type = "rss"
565
+ when "rss"
566
+ @feed_type = "rss"
567
+ when "channel"
568
+ @feed_type = "cdf"
569
+ end
570
+ end
571
+ return @feed_type
572
+ end
573
+
574
+ # Sets the default feed type
575
+ def feed_type=(new_feed_type)
576
+ @feed_type = new_feed_type
577
+ end
578
+
579
+ # Returns the version number of the feed type.
580
+ # Intentionally does not differentiate between the Netscape and Userland
581
+ # versions of RSS 0.91.
582
+ def feed_version
583
+ if @feed_version.nil?
584
+ version = nil
585
+ begin
586
+ version = XPath.first(root_node, "@version").to_s.strip.to_f
587
+ rescue
588
+ end
589
+ version = nil if version == 0.0
590
+ default_namespace = XPath.first(root_node, "@xmlns").to_s.strip
591
+ case self.feed_type
592
+ when "atom"
593
+ if default_namespace == "http://www.w3.org/2005/Atom"
594
+ @feed_version = 1.0
595
+ elsif version != nil
596
+ @feed_version = version
597
+ elsif default_namespace == "http://purl.org/atom/ns#"
598
+ @feed_version = 0.3
599
+ end
600
+ when "rss"
601
+ if default_namespace == "http://my.netscape.com/rdf/simple/0.9/"
602
+ @feed_version = 0.9
603
+ elsif default_namespace == "http://purl.org/rss/1.0/"
604
+ @feed_version = 1.0
605
+ elsif default_namespace == "http://purl.org/net/rss1.1#"
606
+ @feed_version = 1.1
607
+ elsif version != nil
608
+ case version
609
+ when 2.1
610
+ @feed_version = 2.0
611
+ when 2.01
612
+ @feed_version = 2.0
613
+ else
614
+ @feed_version = version
615
+ end
616
+ end
617
+ when "cdf"
618
+ @feed_version = 0.4
619
+ when "!okay/news"
620
+ @feed_version = nil
621
+ end
622
+ end
623
+ return @feed_version
624
+ end
625
+
626
+ # Sets the default feed version
627
+ def feed_version=(new_feed_version)
628
+ @feed_version = new_feed_version
629
+ end
630
+
631
+ # Returns the feed's unique id
632
+ def id
633
+ if @id.nil?
634
+ unless channel_node.nil?
635
+ @id = XPath.first(channel_node, "id/text()").to_s
636
+ if @id == ""
637
+ @id = XPath.first(channel_node, "guid/text()").to_s
638
+ end
639
+ end
640
+ unless root_node.nil?
641
+ if @id == "" || @id.nil?
642
+ @id = XPath.first(root_node, "id/text()").to_s
643
+ end
644
+ if @id == ""
645
+ @id = XPath.first(root_node, "guid/text()").to_s
646
+ end
647
+ end
648
+ @id = nil if @id == ""
649
+ end
650
+ return @id
651
+ end
652
+
653
+ # Sets the feed's unique id
654
+ def id=(new_id)
655
+ @id = new_id
656
+ end
657
+
658
+ # Returns the feed url.
659
+ def url
660
+ if @url.nil? && self.xml_data != nil
661
+ @url = XPath.first(channel_node, "link[@rel='self']/@href").to_s
662
+ @url = nil if @url == ""
663
+ end
664
+ return @url
665
+ end
666
+
667
+ # Sets the feed url and prepares the cache_object if necessary.
668
+ def url=(new_url)
669
+ @url = FeedTools.normalize_url(new_url)
670
+ self.cache_object.url = new_url unless self.cache_object.nil?
671
+ end
672
+
673
+ # Returns the feed title
674
+ def title
675
+ if @title.nil?
676
+ unless channel_node.nil?
677
+ repair_entities = false
678
+ title_node = XPath.first(channel_node, "title")
679
+ if title_node.nil?
680
+ title_node = XPath.first(channel_node, "dc:title")
681
+ end
682
+ if title_node.nil?
683
+ title_node = XPath.first(channel_node, "TITLE")
684
+ end
685
+ end
686
+ if title_node.nil?
687
+ return nil
688
+ end
689
+ if XPath.first(title_node, "@type").to_s == "xhtml" ||
690
+ XPath.first(title_node, "@mode").to_s == "xhtml" ||
691
+ XPath.first(title_node, "@type").to_s == "xml" ||
692
+ XPath.first(title_node, "@mode").to_s == "xml" ||
693
+ XPath.first(title_node, "@type").to_s == "application/xhtml+xml"
694
+ @title = title_node.inner_xml
695
+ elsif XPath.first(title_node, "@type").to_s == "escaped" ||
696
+ XPath.first(title_node, "@mode").to_s == "escaped"
697
+ @title = FeedTools.unescape_entities(
698
+ XPath.first(title_node, "text()").to_s)
699
+ else
700
+ @title = title_node.inner_xml
701
+ repair_entities = true
702
+ end
703
+ unless @title.nil?
704
+ @title = FeedTools.sanitize_html(@title, :strip)
705
+ @title = FeedTools.unescape_entities(@title) if repair_entities
706
+ @title = FeedTools.tidy_html(@title)
707
+ end
708
+ @title.gsub!(/\n/, " ")
709
+ @title.strip!
710
+ @title = nil if @title == ""
711
+ self.cache_object.title = @title unless self.cache_object.nil?
712
+ end
713
+ return @title
714
+ end
715
+
716
+ # Sets the feed title
717
+ def title=(new_title)
718
+ @title = new_title
719
+ self.cache_object.title = new_title unless self.cache_object.nil?
720
+ end
721
+
722
+ # Returns the feed description
723
+ def description
724
+ if @description.nil?
725
+ unless channel_node.nil?
726
+ repair_entities = false
727
+ description_node = XPath.first(channel_node, "description")
728
+ if description_node.nil?
729
+ description_node = XPath.first(channel_node, "tagline")
730
+ end
731
+ if description_node.nil?
732
+ description_node = XPath.first(channel_node, "subtitle")
733
+ end
734
+ if description_node.nil?
735
+ description_node = XPath.first(channel_node, "summary")
736
+ end
737
+ if description_node.nil?
738
+ description_node = XPath.first(channel_node, "abstract")
739
+ end
740
+ if description_node.nil?
741
+ description_node = XPath.first(channel_node, "ABSTRACT")
742
+ end
743
+ if description_node.nil?
744
+ description_node = XPath.first(channel_node, "info")
745
+ end
746
+ if description_node.nil?
747
+ description_node = XPath.first(channel_node, "content:encoded")
748
+ @bozo = true unless description_node.nil?
749
+ end
750
+ if description_node.nil?
751
+ description_node = XPath.first(channel_node, "content")
752
+ @bozo = true unless description_node.nil?
753
+ end
754
+ if description_node.nil?
755
+ description_node = XPath.first(channel_node, "xhtml:body")
756
+ @bozo = true unless description_node.nil?
757
+ end
758
+ if description_node.nil?
759
+ description_node = XPath.first(channel_node, "body")
760
+ @bozo = true unless description_node.nil?
761
+ end
762
+ end
763
+ if description_node.nil?
764
+ return nil
765
+ end
766
+ unless description_node.nil?
767
+ if XPath.first(description_node, "@encoding").to_s != ""
768
+ @description =
769
+ "[Embedded data objects are not currently supported.]"
770
+ elsif XPath.first(description_node, "@type").to_s == "xhtml" ||
771
+ XPath.first(description_node, "@mode").to_s == "xhtml" ||
772
+ XPath.first(description_node, "@type").to_s == "xml" ||
773
+ XPath.first(description_node, "@mode").to_s == "xml" ||
774
+ XPath.first(description_node, "@type").to_s ==
775
+ "application/xhtml+xml"
776
+ @description = description_node.inner_xml
777
+ elsif XPath.first(description_node, "@type").to_s == "escaped" ||
778
+ XPath.first(description_node, "@mode").to_s == "escaped"
779
+ @description = FeedTools.unescape_entities(
780
+ description_node.inner_xml)
781
+ else
782
+ @description = description_node.inner_xml
783
+ repair_entities = true
784
+ end
785
+ end
786
+ if @description == ""
787
+ @description = self.itunes_summary
788
+ @description = "" if @description.nil?
789
+ end
790
+ if @description == ""
791
+ @description = self.itunes_subtitle
792
+ @description = "" if @description.nil?
793
+ end
794
+
795
+ unless @description.nil?
796
+ @description = FeedTools.sanitize_html(@description, :strip)
797
+ @description = FeedTools.unescape_entities(@description) if repair_entities
798
+ @description = FeedTools.tidy_html(@description)
799
+ end
800
+
801
+ @description = @description.strip unless @description.nil?
802
+ @description = nil if @description == ""
803
+ end
804
+ return @description
805
+ end
806
+
807
+ # Sets the feed description
808
+ def description=(new_description)
809
+ @description = new_description
810
+ end
811
+
812
+ # Returns the contents of the itunes:summary element
813
+ def itunes_summary
814
+ if @itunes_summary.nil?
815
+ unless channel_node.nil?
816
+ @itunes_summary = FeedTools.unescape_entities(XPath.first(channel_node,
817
+ "itunes:summary/text()").to_s)
818
+ end
819
+ unless root_node.nil?
820
+ if @itunes_summary == "" || @itunes_summary.nil?
821
+ @itunes_summary = FeedTools.unescape_entities(XPath.first(root_node,
822
+ "itunes:summary/text()").to_s)
823
+ end
824
+ end
825
+ if @itunes_summary == ""
826
+ @itunes_summary = nil
827
+ end
828
+ @itunes_summary =
829
+ FeedTools.sanitize_html(@itunes_summary) unless @itunes_summary.nil?
830
+ end
831
+ return @itunes_summary
832
+ end
833
+
834
+ # Sets the contents of the itunes:summary element
835
+ def itunes_summary=(new_itunes_summary)
836
+ @itunes_summary = new_itunes_summary
837
+ end
838
+
839
+ # Returns the contents of the itunes:subtitle element
840
+ def itunes_subtitle
841
+ if @itunes_subtitle.nil?
842
+ unless channel_node.nil?
843
+ @itunes_subtitle = FeedTools.unescape_entities(XPath.first(channel_node,
844
+ "itunes:subtitle/text()").to_s)
845
+ end
846
+ unless root_node.nil?
847
+ if @itunes_subtitle == "" || @itunes_subtitle.nil?
848
+ @itunes_subtitle = FeedTools.unescape_entities(XPath.first(root_node,
849
+ "itunes:subtitle/text()").to_s)
850
+ end
851
+ end
852
+ if @itunes_subtitle == ""
853
+ @itunes_subtitle = nil
854
+ end
855
+ unless @itunes_subtitle.nil?
856
+ @itunes_subtitle = FeedTools.sanitize_html(@itunes_subtitle)
857
+ end
858
+ end
859
+ return @itunes_subtitle
860
+ end
861
+
862
+ # Sets the contents of the itunes:subtitle element
863
+ def itunes_subtitle=(new_itunes_subtitle)
864
+ @itunes_subtitle = new_itunes_subtitle
865
+ end
866
+
867
+ # Returns the feed link
868
+ def link
869
+ if @link.nil?
870
+ unless channel_node.nil?
871
+ # get the feed link from the xml document
872
+ @link = XPath.first(channel_node, "link[@rel='alternate' @type='text/html']/@href").to_s
873
+ if @link == ""
874
+ @link = XPath.first(channel_node, "link[@rel='alternate']/@href").to_s
875
+ end
876
+ if @link == ""
877
+ @link = XPath.first(channel_node, "link/@href").to_s
878
+ end
879
+ if @link == ""
880
+ @link = XPath.first(channel_node, "link/text()").to_s
881
+ end
882
+ if @link == ""
883
+ @link = XPath.first(channel_node, "@href").to_s
884
+ end
885
+ if @link == ""
886
+ @link = XPath.first(channel_node, "@HREF").to_s
887
+ end
888
+ if @link == ""
889
+ @link = XPath.first(channel_node, "a/@href").to_s
890
+ end
891
+ if @link == ""
892
+ @link = XPath.first(channel_node, "A/@HREF").to_s
893
+ end
894
+ end
895
+ if @link == "" || @link.nil?
896
+ if FeedTools.is_uri? self.guid
897
+ @link = self.guid
898
+ end
899
+ end
900
+ if @link == "" && channel_node != nil
901
+ # Technically, we shouldn't use the base attribute for this, but if the href attribute
902
+ # is missing, it's already a given that we're looking at a messed up CDF file. We can
903
+ # always pray it's correct.
904
+ @link = XPath.first(channel_node, "@base").to_s
905
+ end
906
+ @link = FeedTools.normalize_url(@link)
907
+ unless self.cache_object.nil?
908
+ self.cache_object.link = @link
909
+ end
910
+ end
911
+ return @link
912
+ end
913
+
914
+ # Sets the feed link
915
+ def link=(new_link)
916
+ @link = new_link
917
+ unless self.cache_object.nil?
918
+ self.cache_object.link = new_link
919
+ end
920
+ end
921
+
922
+ # Returns the url to the icon file for this feed.
923
+ #
924
+ # This method uses the url from the link field in order to avoid grabbing
925
+ # the favicon for services like feedburner.
926
+ def icon
927
+ if @icon.nil?
928
+ icon_node = XPath.first(channel_node, "link[@rel='icon']")
929
+ if icon_node.nil?
930
+ icon_node = XPath.first(channel_node, "link[@rel='shortcut icon']")
931
+ end
932
+ if icon_node.nil?
933
+ icon_node = XPath.first(channel_node, "link[@type='image/x-icon']")
934
+ end
935
+ if icon_node.nil?
936
+ icon_node = XPath.first(channel_node, "icon")
937
+ end
938
+ if icon_node.nil?
939
+ icon_node = XPath.first(channel_node, "logo[@style='icon']")
940
+ end
941
+ if icon_node.nil?
942
+ icon_node = XPath.first(channel_node, "LOGO[@STYLE='ICON']")
943
+ end
944
+ unless icon_node.nil?
945
+ @icon = FeedTools.unescape_entities(
946
+ XPath.first(icon_node, "@href").to_s)
947
+ if @icon == ""
948
+ @icon = FeedTools.unescape_entities(
949
+ XPath.first(icon_node, "text()").to_s)
950
+ unless FeedTools.is_uri? @icon
951
+ @icon = ""
952
+ end
953
+ end
954
+ if @icon == "" && self.link != nil && self.link != ""
955
+ link_uri = URI.parse(FeedTools.normalize_url(self.link))
956
+ @icon =
957
+ link_uri.scheme + "://" + link_uri.host + "/favicon.ico"
958
+ end
959
+ @icon = nil if @icon == ""
960
+ end
961
+ end
962
+ return @icon
963
+ end
964
+
965
+ # Returns the feed author
966
+ def author
967
+ if @author.nil?
968
+ @author = FeedTools::Feed::Author.new
969
+ unless channel_node.nil?
970
+ author_node = XPath.first(channel_node, "author")
971
+ if author_node.nil?
972
+ author_node = XPath.first(channel_node, "managingEditor")
973
+ end
974
+ if author_node.nil?
975
+ author_node = XPath.first(channel_node, "dc:author")
976
+ end
977
+ if author_node.nil?
978
+ author_node = XPath.first(channel_node, "dc:creator")
979
+ end
980
+ if author_node.nil?
981
+ author_node = XPath.first(channel_node, "atom:author")
982
+ end
983
+ end
984
+ unless author_node.nil?
985
+ @author.raw = FeedTools.unescape_entities(
986
+ XPath.first(author_node, "text()").to_s)
987
+ @author.raw = nil if @author.raw == ""
988
+ unless @author.raw.nil?
989
+ raw_scan = @author.raw.scan(
990
+ /(.*)\((\b[A-Z0-9._%-\+]+@[A-Z0-9._%-]+\.[A-Z]{2,4}\b)\)/i)
991
+ if raw_scan.nil? || raw_scan.size == 0
992
+ raw_scan = @author.raw.scan(
993
+ /(\b[A-Z0-9._%-\+]+@[A-Z0-9._%-]+\.[A-Z]{2,4}\b)\s*\((.*)\)/i)
994
+ author_raw_pair = raw_scan.first.reverse unless raw_scan.size == 0
995
+ else
996
+ author_raw_pair = raw_scan.first
997
+ end
998
+ if raw_scan.nil? || raw_scan.size == 0
999
+ email_scan = @author.raw.scan(
1000
+ /\b[A-Z0-9._%-\+]+@[A-Z0-9._%-]+\.[A-Z]{2,4}\b/i)
1001
+ if email_scan != nil && email_scan.size > 0
1002
+ @author.email = email_scan.first.strip
1003
+ end
1004
+ end
1005
+ unless author_raw_pair.nil? || author_raw_pair.size == 0
1006
+ @author.name = author_raw_pair.first.strip
1007
+ @author.email = author_raw_pair.last.strip
1008
+ else
1009
+ unless @author.raw.include?("@")
1010
+ # We can be reasonably sure we are looking at something
1011
+ # that the creator didn't intend to contain an email address if
1012
+ # it got through the preceeding regexes and it doesn't
1013
+ # contain the tell-tale '@' symbol.
1014
+ @author.name = @author.raw
1015
+ end
1016
+ end
1017
+ end
1018
+ @author.name = "" if @author.name.nil?
1019
+ if @author.name == ""
1020
+ @author.name = FeedTools.unescape_entities(
1021
+ XPath.first(author_node, "name/text()").to_s)
1022
+ end
1023
+ if @author.name == ""
1024
+ @author.name = FeedTools.unescape_entities(
1025
+ XPath.first(author_node, "@name").to_s)
1026
+ end
1027
+ if @author.email == ""
1028
+ @author.email = FeedTools.unescape_entities(
1029
+ XPath.first(author_node, "email/text()").to_s)
1030
+ end
1031
+ if @author.email == ""
1032
+ @author.email = FeedTools.unescape_entities(
1033
+ XPath.first(author_node, "@email").to_s)
1034
+ end
1035
+ if @author.url == ""
1036
+ @author.url = FeedTools.unescape_entities(
1037
+ XPath.first(author_node, "url/text()").to_s)
1038
+ end
1039
+ if @author.url == ""
1040
+ @author.url = FeedTools.unescape_entities(
1041
+ XPath.first(author_node, "@url").to_s)
1042
+ end
1043
+ @author.name = nil if @author.name == ""
1044
+ @author.raw = nil if @author.raw == ""
1045
+ @author.email = nil if @author.email == ""
1046
+ @author.url = nil if @author.url == ""
1047
+ end
1048
+ # Fallback on the itunes module if we didn't find an author name
1049
+ begin
1050
+ @author.name = self.itunes_author if @author.name.nil?
1051
+ rescue
1052
+ @author.name = nil
1053
+ end
1054
+ end
1055
+ return @author
1056
+ end
1057
+
1058
+ # Sets the feed author
1059
+ def author=(new_author)
1060
+ if new_author.respond_to?(:name) &&
1061
+ new_author.respond_to?(:email) &&
1062
+ new_author.respond_to?(:url)
1063
+ # It's a complete author object, just set it.
1064
+ @author = new_author
1065
+ else
1066
+ # We're not looking at an author object, this is probably a string,
1067
+ # default to setting the author's name.
1068
+ if @author.nil?
1069
+ @author = FeedTools::Feed::Author.new
1070
+ end
1071
+ @author.name = new_author
1072
+ end
1073
+ end
1074
+
1075
+ # Returns the feed publisher
1076
+ def publisher
1077
+ if @publisher.nil?
1078
+ @publisher = FeedTools::Feed::Author.new
1079
+
1080
+ # Set the author name
1081
+ @publisher.raw = FeedTools.unescape_entities(
1082
+ XPath.first(channel_node, "dc:publisher/text()").to_s)
1083
+ if @publisher.raw == ""
1084
+ @publisher.raw = FeedTools.unescape_entities(
1085
+ XPath.first(channel_node, "webMaster/text()").to_s)
1086
+ end
1087
+ unless @publisher.raw == ""
1088
+ raw_scan = @publisher.raw.scan(
1089
+ /(.*)\((\b[A-Z0-9._%-\+]+@[A-Z0-9._%-]+\.[A-Z]{2,4}\b)\)/i)
1090
+ if raw_scan.nil? || raw_scan.size == 0
1091
+ raw_scan = @publisher.raw.scan(
1092
+ /(\b[A-Z0-9._%-\+]+@[A-Z0-9._%-]+\.[A-Z]{2,4}\b)\s*\((.*)\)/i)
1093
+ unless raw_scan.size == 0
1094
+ publisher_raw_pair = raw_scan.first.reverse
1095
+ end
1096
+ else
1097
+ publisher_raw_pair = raw_scan.first
1098
+ end
1099
+ if raw_scan.nil? || raw_scan.size == 0
1100
+ email_scan = @publisher.raw.scan(
1101
+ /\b[A-Z0-9._%-\+]+@[A-Z0-9._%-]+\.[A-Z]{2,4}\b/i)
1102
+ if email_scan != nil && email_scan.size > 0
1103
+ @publisher.email = email_scan.first.strip
1104
+ end
1105
+ end
1106
+ unless publisher_raw_pair.nil? || publisher_raw_pair.size == 0
1107
+ @publisher.name = publisher_raw_pair.first.strip
1108
+ @publisher.email = publisher_raw_pair.last.strip
1109
+ else
1110
+ unless @publisher.raw.include?("@")
1111
+ # We can be reasonably sure we are looking at something
1112
+ # that the creator didn't intend to contain an email address if
1113
+ # it got through the preceeding regexes and it doesn't
1114
+ # contain the tell-tale '@' symbol.
1115
+ @publisher.name = @publisher.raw
1116
+ end
1117
+ end
1118
+ end
1119
+
1120
+ @publisher.name = nil if @publisher.name == ""
1121
+ @publisher.raw = nil if @publisher.raw == ""
1122
+ @publisher.email = nil if @publisher.email == ""
1123
+ @publisher.url = nil if @publisher.url == ""
1124
+ end
1125
+ return @publisher
1126
+ end
1127
+
1128
+ # Sets the feed publisher
1129
+ def publisher=(new_publisher)
1130
+ if new_publisher.respond_to?(:name) &&
1131
+ new_publisher.respond_to?(:email) &&
1132
+ new_publisher.respond_to?(:url)
1133
+ # It's a complete Author object, just set it.
1134
+ @publisher = new_publisher
1135
+ else
1136
+ # We're not looking at an Author object, this is probably a string,
1137
+ # default to setting the publisher's name.
1138
+ if @publisher.nil?
1139
+ @publisher = FeedTools::Feed::Author.new
1140
+ end
1141
+ @publisher.name = new_publisher
1142
+ end
1143
+ end
1144
+
1145
+ # Returns the contents of the itunes:author element
1146
+ #
1147
+ # Returns any incorrectly placed channel-level itunes:author
1148
+ # elements. They're actually amazingly common. People don't read specs.
1149
+ # There is no setter for this, since this is a "bozo" attribute.
1150
+ def itunes_author
1151
+ if @itunes_author.nil?
1152
+ @itunes_author = FeedTools.unescape_entities(XPath.first(channel_node,
1153
+ "itunes:author/text()").to_s)
1154
+ @itunes_author = nil if @itunes_author == ""
1155
+ @bozo = true unless @itunes_author.nil?
1156
+ end
1157
+ return @itunes_author
1158
+ end
1159
+
1160
+ # Returns the feed item time
1161
+ def time
1162
+ if @time.nil?
1163
+ unless channel_node.nil?
1164
+ time_string = XPath.first(channel_node, "pubDate/text()").to_s
1165
+ if time_string == ""
1166
+ time_string = XPath.first(channel_node, "dc:date/text()").to_s
1167
+ end
1168
+ if time_string == ""
1169
+ time_string = XPath.first(channel_node, "issued/text()").to_s
1170
+ end
1171
+ if time_string == ""
1172
+ time_string = XPath.first(channel_node, "updated/text()").to_s
1173
+ end
1174
+ if time_string == ""
1175
+ time_string = XPath.first(channel_node, "time/text()").to_s
1176
+ end
1177
+ end
1178
+ begin
1179
+ if time_string != nil && time_string != ""
1180
+ @time = Time.parse(time_string) rescue self.succ_time
1181
+ elsif time_string == nil
1182
+ @time = self.succ_time
1183
+ end
1184
+ if @time == nil
1185
+ @time = Time.now
1186
+ end
1187
+ rescue
1188
+ @time = Time.now
1189
+ end
1190
+ end
1191
+ return @time
1192
+ end
1193
+
1194
+ # Sets the feed item time
1195
+ def time=(new_time)
1196
+ @time = new_time
1197
+ end
1198
+
1199
+ # Returns 1 second after the previous item's time.
1200
+ def succ_time #:nodoc:
1201
+ begin
1202
+ if feed.nil?
1203
+ return nil
1204
+ end
1205
+ feed.items
1206
+ unsorted_items = feed.instance_variable_get("@items")
1207
+ item_index = unsorted_items.index(self)
1208
+ if item_index.nil?
1209
+ return nil
1210
+ end
1211
+ if item_index <= 0
1212
+ return Time.now
1213
+ end
1214
+ previous_item = unsorted_items[item_index - 1]
1215
+ return previous_item.time.succ
1216
+ rescue
1217
+ return nil
1218
+ end
1219
+ end
1220
+ private :succ_time
1221
+
1222
+ # Returns the feed item updated time
1223
+ def updated
1224
+ if @updated.nil?
1225
+ unless channel_node.nil?
1226
+ updated_string = XPath.first(channel_node, "updated/text()").to_s
1227
+ if updated_string == ""
1228
+ updated_string = XPath.first(channel_node, "modified/text()").to_s
1229
+ end
1230
+ end
1231
+ if updated_string != nil && updated_string != ""
1232
+ @updated = Time.parse(updated_string) rescue nil
1233
+ else
1234
+ @updated = nil
1235
+ end
1236
+ end
1237
+ return @updated
1238
+ end
1239
+
1240
+ # Sets the feed item updated time
1241
+ def updated=(new_updated)
1242
+ @updated = new_updated
1243
+ end
1244
+
1245
+ # Returns the feed item issued time
1246
+ def issued
1247
+ if @issued.nil?
1248
+ unless channel_node.nil?
1249
+ issued_string = XPath.first(channel_node, "issued/text()").to_s
1250
+ if issued_string == ""
1251
+ issued_string = XPath.first(channel_node, "pubDate/text()").to_s
1252
+ end
1253
+ if issued_string == ""
1254
+ issued_string = XPath.first(channel_node, "dc:date/text()").to_s
1255
+ end
1256
+ if issued_string == ""
1257
+ issued_string = XPath.first(channel_node, "published/text()").to_s
1258
+ end
1259
+ end
1260
+ if issued_string != nil && issued_string != ""
1261
+ @issued = Time.parse(issued_string) rescue nil
1262
+ else
1263
+ @issued = nil
1264
+ end
1265
+ end
1266
+ return @issued
1267
+ end
1268
+
1269
+ # Sets the feed item issued time
1270
+ def issued=(new_issued)
1271
+ @issued = new_issued
1272
+ end
1273
+
1274
+ # Returns the feed item published time
1275
+ def published
1276
+ if @published.nil?
1277
+ unless channel_node.nil?
1278
+ published_string = XPath.first(channel_node, "published/text()").to_s
1279
+ if published_string == ""
1280
+ published_string = XPath.first(channel_node, "pubDate/text()").to_s
1281
+ end
1282
+ if published_string == ""
1283
+ published_string = XPath.first(channel_node, "dc:date/text()").to_s
1284
+ end
1285
+ if published_string == ""
1286
+ published_string = XPath.first(channel_node, "issued/text()").to_s
1287
+ end
1288
+ end
1289
+ if published_string != nil && published_string != ""
1290
+ @published = Time.parse(published_string) rescue nil
1291
+ else
1292
+ @published = nil
1293
+ end
1294
+ end
1295
+ return @published
1296
+ end
1297
+
1298
+ # Sets the feed item published time
1299
+ def published=(new_published)
1300
+ @published = new_published
1301
+ end
1302
+
1303
+ # Returns a list of the feed's categories
1304
+ def categories
1305
+ if @categories.nil?
1306
+ @categories = []
1307
+ category_nodes = XPath.match(channel_node, "category")
1308
+ if category_nodes.nil? || category_nodes.empty?
1309
+ category_nodes = XPath.match(channel_node, "dc:subject")
1310
+ end
1311
+ unless category_nodes.nil?
1312
+ for category_node in category_nodes
1313
+ category = FeedTools::Feed::Category.new
1314
+ category.term = XPath.first(category_node, "@term").to_s
1315
+ if category.term == ""
1316
+ category.term = XPath.first(category_node, "text()").to_s
1317
+ end
1318
+ category.term.strip! unless category.term.nil?
1319
+ category.term = nil if category.term == ""
1320
+ category.label = XPath.first(category_node, "@label").to_s
1321
+ category.label.strip! unless category.label.nil?
1322
+ category.label = nil if category.label == ""
1323
+ category.scheme = XPath.first(category_node, "@scheme").to_s
1324
+ if category.scheme == ""
1325
+ category.scheme = XPath.first(category_node, "@domain").to_s
1326
+ end
1327
+ category.scheme.strip! unless category.scheme.nil?
1328
+ category.scheme = nil if category.scheme == ""
1329
+ @categories << category
1330
+ end
1331
+ end
1332
+ end
1333
+ return @categories
1334
+ end
1335
+
1336
+ # Returns a list of the feed's images
1337
+ def images
1338
+ if @images.nil?
1339
+ @images = []
1340
+ unless channel_node.nil?
1341
+ image_nodes = XPath.match(channel_node, "image")
1342
+ if image_nodes.nil? || image_nodes.empty?
1343
+ image_nodes = XPath.match(channel_node, "link")
1344
+ end
1345
+ if image_nodes.nil? || image_nodes.empty?
1346
+ image_nodes = XPath.match(channel_node, "logo")
1347
+ end
1348
+ if image_nodes.nil? || image_nodes.empty?
1349
+ image_nodes = XPath.match(channel_node, "LOGO")
1350
+ end
1351
+ unless image_nodes.nil?
1352
+ for image_node in image_nodes
1353
+ image = FeedTools::Feed::Image.new
1354
+ image.url = XPath.first(image_node, "url/text()").to_s
1355
+ if image.url == ""
1356
+ image.url = XPath.first(image_node, "@rdf:resource").to_s
1357
+ end
1358
+ if image.url == "" && (image_node.name == "logo" ||
1359
+ (image_node.attributes['type'] =~ /^image/) == 0)
1360
+ image.url = XPath.first(image_node, "@href").to_s
1361
+ end
1362
+ if image.url == "" && image_node.name == "LOGO"
1363
+ image.url = XPath.first(image_node, "@HREF").to_s
1364
+ end
1365
+ image.url.strip! unless image.url.nil?
1366
+ image.url = nil if image.url == ""
1367
+ image.title = XPath.first(image_node, "title/text()").to_s
1368
+ image.title.strip! unless image.title.nil?
1369
+ image.title = nil if image.title == ""
1370
+ image.description =
1371
+ XPath.first(image_node, "description/text()").to_s
1372
+ image.description.strip! unless image.description.nil?
1373
+ image.description = nil if image.description == ""
1374
+ image.link = XPath.first(image_node, "link/text()").to_s
1375
+ image.link.strip! unless image.link.nil?
1376
+ image.link = nil if image.link == ""
1377
+ image.height = XPath.first(image_node, "height/text()").to_s.to_i
1378
+ image.height = nil if image.height <= 0
1379
+ image.width = XPath.first(image_node, "width/text()").to_s.to_i
1380
+ image.width = nil if image.width <= 0
1381
+ image.style = XPath.first(image_node, "@style").to_s.downcase
1382
+ if image.style == ""
1383
+ image.style = XPath.first(image_node, "@STYLE").to_s.downcase
1384
+ end
1385
+ image.style.strip! unless image.style.nil?
1386
+ image.style = nil if image.style == ""
1387
+ @images << image
1388
+ end
1389
+ end
1390
+ end
1391
+ end
1392
+ return @images
1393
+ end
1394
+
1395
+ # Returns the feed's text input field
1396
+ def text_input
1397
+ if @text_input.nil?
1398
+ @text_input = FeedTools::Feed::TextInput.new
1399
+ text_input_node = XPath.first(channel_node, "textInput")
1400
+ unless text_input_node.nil?
1401
+ @text_input.title =
1402
+ XPath.first(text_input_node, "title/text()").to_s
1403
+ @text_input.title = nil if @text_input.title == ""
1404
+ @text_input.description =
1405
+ XPath.first(text_input_node, "description/text()").to_s
1406
+ @text_input.description = nil if @text_input.description == ""
1407
+ @text_input.link =
1408
+ XPath.first(text_input_node, "link/text()").to_s
1409
+ @text_input.link = nil if @text_input.link == ""
1410
+ @text_input.name =
1411
+ XPath.first(text_input_node, "name/text()").to_s
1412
+ @text_input.name = nil if @text_input.name == ""
1413
+ end
1414
+ end
1415
+ return @text_input
1416
+ end
1417
+
1418
+ # Returns the feed's copyright information
1419
+ def copyright
1420
+ if @copyright.nil?
1421
+ unless channel_node.nil?
1422
+ @copyright = XPath.first(channel_node, "copyright/text()").to_s
1423
+ if @copyright == ""
1424
+ @copyright = XPath.first(channel_node, "rights/text()").to_s
1425
+ end
1426
+ if @copyright == ""
1427
+ @copyright = XPath.first(channel_node, "dc:rights/text()").to_s
1428
+ end
1429
+ if @copyright == ""
1430
+ @copyright = XPath.first(channel_node, "copyrights/text()").to_s
1431
+ end
1432
+ @copyright = FeedTools.sanitize_html(@copyright, :strip)
1433
+ @copyright = nil if @copyright == ""
1434
+ end
1435
+ end
1436
+ return @copyright
1437
+ end
1438
+
1439
+ # Sets the feed's copyright information
1440
+ def copyright=(new_copyright)
1441
+ @copyright = new_copyright
1442
+ end
1443
+
1444
+ # Returns the number of seconds before the feed should expire
1445
+ def time_to_live
1446
+ if @time_to_live.nil?
1447
+ unless channel_node.nil?
1448
+ # get the feed time to live from the xml document
1449
+ update_frequency = XPath.first(channel_node, "syn:updateFrequency/text()").to_s
1450
+ if update_frequency != ""
1451
+ update_period = XPath.first(channel_node, "syn:updatePeriod/text()").to_s
1452
+ if update_period == "daily"
1453
+ @time_to_live = update_frequency.to_i.day
1454
+ elsif update_period == "weekly"
1455
+ @time_to_live = update_frequency.to_i.week
1456
+ elsif update_period == "monthly"
1457
+ @time_to_live = update_frequency.to_i.month
1458
+ elsif update_period == "yearly"
1459
+ @time_to_live = update_frequency.to_i.year
1460
+ else
1461
+ # hourly
1462
+ @time_to_live = update_frequency.to_i.hour
1463
+ end
1464
+ end
1465
+ if @time_to_live.nil?
1466
+ # usually expressed in minutes
1467
+ update_frequency = XPath.first(channel_node, "ttl/text()").to_s
1468
+ if update_frequency != ""
1469
+ update_span = XPath.first(channel_node, "ttl/@span").to_s
1470
+ if update_span == "seconds"
1471
+ @time_to_live = update_frequency.to_i
1472
+ elsif update_span == "minutes"
1473
+ @time_to_live = update_frequency.to_i.minute
1474
+ elsif update_span == "hours"
1475
+ @time_to_live = update_frequency.to_i.hour
1476
+ elsif update_span == "days"
1477
+ @time_to_live = update_frequency.to_i.day
1478
+ elsif update_span == "weeks"
1479
+ @time_to_live = update_frequency.to_i.week
1480
+ elsif update_span == "months"
1481
+ @time_to_live = update_frequency.to_i.month
1482
+ elsif update_span == "years"
1483
+ @time_to_live = update_frequency.to_i.year
1484
+ elsif update_frequency.to_i >= 3000
1485
+ # Normally, this should default to minutes, but realistically,
1486
+ # if they meant minutes, you're rarely going to see a value higher
1487
+ # than 120. If we see >= 3000, we're either dealing with a stupid
1488
+ # pseudo-spec that decided to use seconds, or we're looking at
1489
+ # someone who only has weekly updated content. Worst case, we
1490
+ # misreport the time, and we update too often. Best case, we
1491
+ # avoid accidentally updating the feed only once a year. In the
1492
+ # interests of being pragmatic, and since the problem we avoid
1493
+ # is a far greater one than the one we cause, just run the check
1494
+ # and hope no one actually gets hurt.
1495
+ @time_to_live = update_frequency.to_i
1496
+ else
1497
+ @time_to_live = update_frequency.to_i.minute
1498
+ end
1499
+ end
1500
+ end
1501
+ if @time_to_live.nil?
1502
+ @time_to_live = 0
1503
+ update_frequency_days =
1504
+ XPath.first(channel_node, "schedule/intervaltime/@days").to_s
1505
+ update_frequency_hours =
1506
+ XPath.first(channel_node, "schedule/intervaltime/@hour").to_s
1507
+ update_frequency_minutes =
1508
+ XPath.first(channel_node, "schedule/intervaltime/@min").to_s
1509
+ update_frequency_seconds =
1510
+ XPath.first(channel_node, "schedule/intervaltime/@sec").to_s
1511
+ if update_frequency_days != ""
1512
+ @time_to_live = @time_to_live + update_frequency_days.to_i.day
1513
+ end
1514
+ if update_frequency_hours != ""
1515
+ @time_to_live = @time_to_live + update_frequency_hours.to_i.hour
1516
+ end
1517
+ if update_frequency_minutes != ""
1518
+ @time_to_live = @time_to_live + update_frequency_minutes.to_i.minute
1519
+ end
1520
+ if update_frequency_seconds != ""
1521
+ @time_to_live = @time_to_live + update_frequency_seconds.to_i
1522
+ end
1523
+ if @time_to_live == 0
1524
+ @time_to_live = 1.hour
1525
+ end
1526
+ end
1527
+ end
1528
+ end
1529
+ if @time_to_live.nil? || @time_to_live == 0
1530
+ # Default to one hour
1531
+ @time_to_live = 1.hour
1532
+ end
1533
+ @time_to_live = @time_to_live.round
1534
+ return @time_to_live
1535
+ end
1536
+
1537
+ # Sets the feed time to live
1538
+ def time_to_live=(new_time_to_live)
1539
+ @time_to_live = new_time_to_live.round
1540
+ @time_to_live = 1.hour if @time_to_live < 1.hour
1541
+ end
1542
+
1543
+ # Returns the feed's cloud
1544
+ def cloud
1545
+ if @cloud.nil?
1546
+ @cloud = FeedTools::Feed::Cloud.new
1547
+ @cloud.domain = XPath.first(channel_node, "cloud/@domain").to_s
1548
+ @cloud.port = XPath.first(channel_node, "cloud/@port").to_s
1549
+ @cloud.path = XPath.first(channel_node, "cloud/@path").to_s
1550
+ @cloud.register_procedure =
1551
+ XPath.first(channel_node, "cloud/@registerProcedure").to_s
1552
+ @cloud.protocol =
1553
+ XPath.first(channel_node, "cloud/@protocol").to_s.downcase
1554
+ @cloud.domain = nil if @cloud.domain == ""
1555
+ @cloud.port = nil if @cloud.port == ""
1556
+ @cloud.port = @cloud.port.to_i unless @cloud.port.nil?
1557
+ @cloud.port = nil if @cloud.port == 0
1558
+ @cloud.path = nil if @cloud.path == ""
1559
+ @cloud.register_procedure = nil if @cloud.register_procedure == ""
1560
+ @cloud.protocol = nil if @cloud.protocol == ""
1561
+ end
1562
+ return @cloud
1563
+ end
1564
+
1565
+ # Sets the feed's cloud
1566
+ def cloud=(new_cloud)
1567
+ @cloud = new_cloud
1568
+ end
1569
+
1570
+ # Returns the feed generator
1571
+ def generator
1572
+ if @generator.nil?
1573
+ @generator = XPath.first(channel_node, "generator/text()").to_s
1574
+ @generator = FeedTools.strip_html(@generator)
1575
+ @generator = nil if @generator == ""
1576
+ end
1577
+ return @generator
1578
+ end
1579
+
1580
+ # Sets the feed generator
1581
+ def generator=(new_generator)
1582
+ @generator = new_generator
1583
+ end
1584
+
1585
+ # Returns the feed docs
1586
+ def docs
1587
+ if @docs.nil?
1588
+ @docs = XPath.first(channel_node, "docs/text()").to_s
1589
+ @docs = FeedTools.strip_html(@docs)
1590
+ @docs = nil if @docs == ""
1591
+ end
1592
+ return @docs
1593
+ end
1594
+
1595
+ # Sets the feed docs
1596
+ def docs=(new_docs)
1597
+ @docs = new_docs
1598
+ end
1599
+
1600
+ # Returns the feed language
1601
+ def language
1602
+ if @language.nil?
1603
+ unless channel_node.nil?
1604
+ @language = XPath.first(channel_node, "language/text()").to_s
1605
+ if @language == ""
1606
+ @language = XPath.first(channel_node, "dc:language/text()").to_s
1607
+ end
1608
+ if @language == ""
1609
+ @language = XPath.first(channel_node, "xml:lang/text()").to_s
1610
+ end
1611
+ if @language == ""
1612
+ @language = XPath.first(root_node, "xml:lang/text()").to_s
1613
+ end
1614
+ end
1615
+ if @language == "" || @language.nil?
1616
+ @language = "en-us"
1617
+ end
1618
+ @language = @language.downcase
1619
+ @language = nil if @language == ""
1620
+ end
1621
+ return @language
1622
+ end
1623
+
1624
+ # Sets the feed language
1625
+ def language=(new_language)
1626
+ @language = new_language
1627
+ end
1628
+
1629
+ # Returns true if this feed contains explicit material.
1630
+ def explicit?
1631
+ if @explicit.nil?
1632
+ if XPath.first(channel_node,
1633
+ "media:adult/text()").to_s.downcase == "true" ||
1634
+ XPath.first(channel_node,
1635
+ "itunes:explicit/text()").to_s.downcase == "yes" ||
1636
+ XPath.first(channel_node,
1637
+ "itunes:explicit/text()").to_s.downcase == "true"
1638
+ @explicit = true
1639
+ else
1640
+ @explicit = false
1641
+ end
1642
+ end
1643
+ return @explicit
1644
+ end
1645
+
1646
+ # Sets whether or not the feed contains explicit material
1647
+ def explicit=(new_explicit)
1648
+ @explicit = (new_explicit ? true : false)
1649
+ end
1650
+
1651
+ # Returns the feed items
1652
+ def items
1653
+ if @items.nil?
1654
+ unless root_node.nil?
1655
+ raw_items = XPath.match(root_node, "item")
1656
+ if raw_items == nil || raw_items == []
1657
+ raw_items = XPath.match(channel_node, "item")
1658
+ end
1659
+ if raw_items == nil || raw_items == []
1660
+ raw_items = XPath.match(channel_node, "ITEM")
1661
+ end
1662
+ if raw_items == nil || raw_items == []
1663
+ raw_items = XPath.match(root_node, "ITEM")
1664
+ end
1665
+ if raw_items == nil || raw_items == []
1666
+ raw_items = XPath.match(channel_node, "entry")
1667
+ end
1668
+ if raw_items == nil || raw_items == []
1669
+ raw_items = XPath.match(root_node, "entry")
1670
+ end
1671
+ end
1672
+
1673
+ # create the individual feed items
1674
+ @items = []
1675
+ if raw_items != nil
1676
+ for item_node in raw_items
1677
+ new_item = FeedItem.new
1678
+ new_item.xml_data = item_node.to_s
1679
+ new_item.feed = self
1680
+ @items << new_item
1681
+ end
1682
+ end
1683
+ end
1684
+
1685
+ # Sort the items
1686
+ @items = @items.sort do |a,b|
1687
+ (b.time or Time.mktime(1970)) <=> (a.time or Time.mktime(1970))
1688
+ end
1689
+ return @items
1690
+ end
1691
+
1692
+ # The time that the feed was last requested from the remote server. Nil if it has
1693
+ # never been pulled, or if it was created from scratch.
1694
+ def last_retrieved
1695
+ unless self.cache_object.nil?
1696
+ @last_retrieved = self.cache_object.last_retrieved
1697
+ end
1698
+ return @last_retrieved
1699
+ end
1700
+
1701
+ # Sets the time that the feed was last updated.
1702
+ def last_retrieved=(new_last_retrieved)
1703
+ @last_retrieved = new_last_retrieved
1704
+ unless self.cache_object.nil?
1705
+ self.cache_object.last_retrieved = new_last_retrieved
1706
+ end
1707
+ end
1708
+
1709
+ # True if this feed contains audio content enclosures
1710
+ def podcast?
1711
+ podcast = false
1712
+ self.items.each do |item|
1713
+ item.enclosures.each do |enclosure|
1714
+ podcast = true if enclosure.audio?
1715
+ end
1716
+ end
1717
+ return podcast
1718
+ end
1719
+
1720
+ # True if this feed contains video content enclosures
1721
+ def vidlog?
1722
+ vidlog = false
1723
+ self.items.each do |item|
1724
+ item.enclosures.each do |enclosure|
1725
+ vidlog = true if enclosure.video?
1726
+ end
1727
+ end
1728
+ return vidlog
1729
+ end
1730
+
1731
+ # True if this feed is malformed somehow
1732
+ def bozo?
1733
+ if @bozo.nil?
1734
+ @bozo = false
1735
+ end
1736
+ return @bozo
1737
+ end
1738
+
1739
+ # True if the feed was not last retrieved from the cache.
1740
+ def live?
1741
+ return @live
1742
+ end
1743
+
1744
+ # True if the feed has expired and must be reacquired from the remote server.
1745
+ def expired?
1746
+ return self.last_retrieved == nil || (self.last_retrieved + self.time_to_live.hour) < Time.now
1747
+ end
1748
+
1749
+ # Forces this feed to expire.
1750
+ def expire!
1751
+ self.last_retrieved = Time.mktime(1970)
1752
+ self.save
1753
+ end
1754
+
1755
+ # A hook method that is called during the feed generation process. Overriding this method
1756
+ # will enable additional content to be inserted into the feed.
1757
+ def build_xml_hook(feed_type, version, xml_builder)
1758
+ return nil
1759
+ end
1760
+
1761
+ # Generates xml based on the content of the feed
1762
+ def build_xml(feed_type=(self.feed_type or "rss"), version=nil,
1763
+ xml_builder=Builder::XmlMarkup.new(:indent => 2))
1764
+ if feed_type == "rss" && (version == nil || version == 0.0)
1765
+ version = 1.0
1766
+ elsif feed_type == "atom" && (version == nil || version == 0.0)
1767
+ version = 0.3
1768
+ end
1769
+ if feed_type == "rss" && (version == 0.9 || version == 1.0 ||
1770
+ version == 1.1)
1771
+ # RDF-based rss format
1772
+ return xml_builder.tag!("rdf:RDF",
1773
+ "xmlns" => "http://purl.org/rss/1.0/",
1774
+ "xmlns:rdf" => "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
1775
+ "xmlns:dc" => "http://purl.org/dc/elements/1.1/",
1776
+ "xmlns:syn" => "http://purl.org/rss/1.0/modules/syndication/",
1777
+ "xmlns:taxo" => "http://purl.org/rss/1.0/modules/taxonomy/",
1778
+ "xmlns:itunes" => "http://www.itunes.com/DTDs/Podcast-1.0.dtd",
1779
+ "xmlns:media" => "http://search.yahoo.com/mrss") do
1780
+ channel_attributes = {}
1781
+ unless self.link.nil?
1782
+ channel_attributes["rdf:about"] = CGI.escapeHTML(self.link)
1783
+ end
1784
+ xml_builder.channel(channel_attributes) do
1785
+ unless title.nil? || title == ""
1786
+ xml_builder.title(title)
1787
+ else
1788
+ xml_builder.title
1789
+ end
1790
+ unless link.nil? || link == ""
1791
+ xml_builder.link(link)
1792
+ else
1793
+ xml_builder.link
1794
+ end
1795
+ unless images.nil? || images.empty?
1796
+ xml_builder.image("rdf:resource" => CGI.escapeHTML(
1797
+ images.first.url))
1798
+ end
1799
+ unless description.nil? || description == ""
1800
+ xml_builder.description(description)
1801
+ else
1802
+ xml_builder.description
1803
+ end
1804
+ unless language.nil? || language == ""
1805
+ xml_builder.tag!("dc:language", language)
1806
+ end
1807
+ xml_builder.tag!("syn:updatePeriod", "hourly")
1808
+ xml_builder.tag!("syn:updateFrequency", (time_to_live / 1.hour).to_s)
1809
+ xml_builder.tag!("syn:updateBase", Time.mktime(1970).iso8601)
1810
+ xml_builder.items do
1811
+ xml_builder.tag!("rdf:Seq") do
1812
+ unless items.nil?
1813
+ for item in items
1814
+ if item.link.nil?
1815
+ raise "Cannot generate an rdf-based feed with a nil item link field."
1816
+ end
1817
+ xml_builder.tag!("rdf:li", "rdf:resource" => CGI.escapeHTML(item.link))
1818
+ end
1819
+ end
1820
+ end
1821
+ end
1822
+ build_xml_hook(feed_type, version, xml_builder)
1823
+ end
1824
+ unless images.nil? || images.empty?
1825
+ best_image = nil
1826
+ for image in self.images
1827
+ if image.link != nil
1828
+ best_image = image
1829
+ break
1830
+ end
1831
+ end
1832
+ best_image = images.first if best_image.nil?
1833
+ xml_builder.image("rdf:about" => CGI.escapeHTML(best_image.url)) do
1834
+ if best_image.title != nil && best_image.title != ""
1835
+ xml_builder.title(best_image.title)
1836
+ elsif self.title != nil && self.title != ""
1837
+ xml_builder.title(self.title)
1838
+ else
1839
+ xml_builder.title
1840
+ end
1841
+ unless best_image.url.nil? || best_image.url == ""
1842
+ xml_builder.url(best_image.url)
1843
+ end
1844
+ if best_image.link != nil && best_image.link != ""
1845
+ xml_builder.link(best_image.link)
1846
+ elsif self.link != nil && self.link != ""
1847
+ xml_builder.link(self.link)
1848
+ else
1849
+ xml_builder.link
1850
+ end
1851
+ end
1852
+ end
1853
+ unless items.nil?
1854
+ for item in items
1855
+ item.build_xml(feed_type, version, xml_builder)
1856
+ end
1857
+ end
1858
+ end
1859
+ elsif feed_type == "rss"
1860
+ # normal rss format
1861
+ return xml_builder.rss("version" => "2.0",
1862
+ "xmlns:rdf" => "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
1863
+ "xmlns:dc" => "http://purl.org/dc/elements/1.1/",
1864
+ "xmlns:taxo" => "http://purl.org/rss/1.0/modules/taxonomy/",
1865
+ "xmlns:trackback" =>
1866
+ "http://madskills.com/public/xml/rss/module/trackback/",
1867
+ "xmlns:itunes" => "http://www.itunes.com/DTDs/Podcast-1.0.dtd",
1868
+ "xmlns:media" => "http://search.yahoo.com/mrss") do
1869
+ xml_builder.channel do
1870
+ unless title.nil? || title == ""
1871
+ xml_builder.title(title)
1872
+ end
1873
+ unless link.nil? || link == ""
1874
+ xml_builder.link(link)
1875
+ end
1876
+ unless description.nil? || description == ""
1877
+ xml_builder.description(description)
1878
+ end
1879
+ xml_builder.ttl((time_to_live / 1.minute).to_s)
1880
+ xml_builder.generator(
1881
+ "http://www.sporkmonger.com/projects/feedtools")
1882
+ build_xml_hook(feed_type, version, xml_builder)
1883
+ unless items.nil?
1884
+ for item in items
1885
+ item.build_xml(feed_type, version, xml_builder)
1886
+ end
1887
+ end
1888
+ end
1889
+ end
1890
+ elsif feed_type == "atom" && version == 0.3
1891
+ # normal atom format
1892
+ return xml_builder.feed("xmlns" => "http://purl.org/atom/ns#",
1893
+ "version" => version,
1894
+ "xml:lang" => language) do
1895
+ unless title.nil? || title == ""
1896
+ xml_builder.title(title,
1897
+ "mode" => "escaped",
1898
+ "type" => "text/html")
1899
+ end
1900
+ xml_builder.author do
1901
+ unless self.author.nil? || self.author.name.nil?
1902
+ xml_builder.name(self.author.name)
1903
+ else
1904
+ xml_builder.name("n/a")
1905
+ end
1906
+ unless self.author.nil? || self.author.email.nil?
1907
+ xml_builder.email(self.author.email)
1908
+ end
1909
+ unless self.author.nil? || self.author.url.nil?
1910
+ xml_builder.url(self.author.url)
1911
+ end
1912
+ end
1913
+ unless link.nil? || link == ""
1914
+ xml_builder.link("href" => link,
1915
+ "rel" => "alternate",
1916
+ "type" => "text/html",
1917
+ "title" => title)
1918
+ end
1919
+ unless description.nil? || description == ""
1920
+ xml_builder.tagline(description,
1921
+ "mode" => "escaped",
1922
+ "type" => "text/html")
1923
+ end
1924
+ xml_builder.generator("FeedTools",
1925
+ "url" => "http://www.sporkmonger.com/projects/feedtools")
1926
+ build_xml_hook(feed_type, version, xml_builder)
1927
+ unless items.nil?
1928
+ for item in items
1929
+ item.build_xml(feed_type, version, xml_builder)
1930
+ end
1931
+ end
1932
+ end
1933
+ elsif feed_type == "atom" && version == 1.0
1934
+ # normal atom format
1935
+ return xml_builder.feed("xmlns" => "http://www.w3.org/2005/Atom",
1936
+ "xml:lang" => language) do
1937
+ unless title.nil? || title == ""
1938
+ xml_builder.title(title,
1939
+ "type" => "html")
1940
+ end
1941
+ xml_builder.author do
1942
+ unless self.author.nil? || self.author.name.nil?
1943
+ xml_builder.name(self.author.name)
1944
+ else
1945
+ xml_builder.name("n/a")
1946
+ end
1947
+ unless self.author.nil? || self.author.email.nil?
1948
+ xml_builder.email(self.author.email)
1949
+ end
1950
+ unless self.author.nil? || self.author.url.nil?
1951
+ xml_builder.url(self.author.url)
1952
+ end
1953
+ end
1954
+ unless self.url.nil? || self.url == ""
1955
+ xml_builder.link("href" => self.url,
1956
+ "rel" => "self",
1957
+ "type" => "application/atom+xml")
1958
+ end
1959
+ unless self.link.nil? || self.link == ""
1960
+ xml_builder.link("href" => self.link,
1961
+ "rel" => "alternate",
1962
+ "type" => "text/html",
1963
+ "title" => self.title)
1964
+ end
1965
+ unless description.nil? || description == ""
1966
+ xml_builder.subtitle(description,
1967
+ "type" => "html")
1968
+ else
1969
+ xml_builder.subtitle(FeedTools.no_content_string,
1970
+ "type" => "html")
1971
+ end
1972
+ if self.updated != nil
1973
+ xml_builder.updated(self.updated.iso8601)
1974
+ elsif self.time != nil
1975
+ # Not technically correct, but a heck of a lot better
1976
+ # than the Time.now fall-back.
1977
+ xml_builder.updated(self.time.iso8601)
1978
+ else
1979
+ xml_builder.updated(Time.now.iso8601)
1980
+ end
1981
+ unless self.published.nil?
1982
+ xml_builder.published(self.published.iso8601)
1983
+ end
1984
+ xml_builder.generator("FeedTools - " +
1985
+ "http://www.sporkmonger.com/projects/feedtools")
1986
+ if self.id != nil
1987
+ unless FeedTools.is_uri? self.id
1988
+ if self.link != nil
1989
+ xml_builder.id(FeedTools.build_urn_uri(self.link))
1990
+ else
1991
+ raise "The unique id must be a valid URI."
1992
+ end
1993
+ else
1994
+ xml_builder.id(self.id)
1995
+ end
1996
+ elsif self.link != nil
1997
+ xml_builder.id(FeedTools.build_urn_uri(self.link))
1998
+ else
1999
+ raise "Cannot build feed, missing feed unique id."
2000
+ end
2001
+ build_xml_hook(feed_type, version, xml_builder)
2002
+ unless items.nil?
2003
+ for item in items
2004
+ item.build_xml(feed_type, version, xml_builder)
2005
+ end
2006
+ end
2007
+ end
2008
+ end
2009
+ end
2010
+
2011
+ # Persists the current feed state to the cache.
2012
+ def save
2013
+ if FeedTools.feed_cache.nil?
2014
+ raise "Caching is currently disabled. Cannot save to cache."
2015
+ elsif self.url.nil?
2016
+ raise "The url field must be set to save to the cache."
2017
+ elsif self.xml_data.nil?
2018
+ raise "The xml_data field must be set to save to the cache."
2019
+ elsif self.cache_object.nil?
2020
+ raise "The cache_object is currently nil. Cannot save to cache."
2021
+ else
2022
+ self.cache_object.url = self.url
2023
+ self.cache_object.title = self.title
2024
+ self.cache_object.link = self.link
2025
+ self.cache_object.xml_data = self.xml_data
2026
+ unless self.http_response.nil?
2027
+ self.cache_object.http_headers = self.http_headers.to_yaml
2028
+ end
2029
+ self.cache_object.last_retrieved = self.last_retrieved
2030
+ self.cache_object.save
2031
+ end
2032
+ end
2033
+
2034
+ alias_method :tagline, :description
2035
+ alias_method :tagline=, :description=
2036
+ alias_method :subtitle, :description
2037
+ alias_method :subtitle=, :description=
2038
+ alias_method :abstract, :description
2039
+ alias_method :abstract=, :description=
2040
+ alias_method :content, :description
2041
+ alias_method :content=, :description=
2042
+ alias_method :ttl, :time_to_live
2043
+ alias_method :ttl=, :time_to_live=
2044
+ alias_method :guid, :id
2045
+ alias_method :guid=, :id=
2046
+ alias_method :entries, :items
2047
+
2048
+ # passes missing methods to the cache_object
2049
+ def method_missing(msg, *params)
2050
+ if self.cache_object.nil?
2051
+ raise NoMethodError, "Invalid method #{msg.to_s}"
2052
+ end
2053
+ return self.cache_object.send(msg, params)
2054
+ end
2055
+
2056
+ # passes missing methods to the FeedTools.feed_cache
2057
+ def Feed.method_missing(msg, *params)
2058
+ if FeedTools.feed_cache.nil?
2059
+ raise NoMethodError, "Invalid method Feed.#{msg.to_s}"
2060
+ end
2061
+ result = FeedTools.feed_cache.send(msg, params)
2062
+ if result.kind_of? FeedTools.feed_cache
2063
+ result = Feed.open(result.url)
2064
+ end
2065
+ return result
2066
+ end
2067
+
2068
+ # Returns a simple representation of the feed object's state.
2069
+ def inspect
2070
+ return "#<FeedTools::Feed:0x#{self.object_id.to_s(16)} URL:#{self.url}>"
2071
+ end
2072
+ end
2073
+ end