feedtools 0.2.17 → 0.2.18

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/CHANGELOG CHANGED
@@ -1,3 +1,11 @@
1
+ == FeedTools 0.2.18
2
+ * no longer ever polls more often than once every 30 minutes
3
+ * fixed overlooked improperly refactored enclosure code
4
+ * fixed issue with inner_xml incorrectly handling xml comments
5
+ * added helper modules
6
+ * test cases now implemented using helpers
7
+ * fixed issue with timeouts
8
+ * fixed stack overflow while estimating timestamps
1
9
  == FeedTools 0.2.17
2
10
  * more fixes for timestamping of feed items
3
11
  * fixed nil bug in root_node, feed_type, feed_version, build_xml
@@ -32,13 +32,13 @@ FEED_TOOLS_ENV = ENV['FEED_TOOLS_ENV'] ||
32
32
  ENV['RAILS_ENV'] ||
33
33
  'production' # :nodoc:
34
34
 
35
- FEED_TOOLS_VERSION = "0.2.17"
35
+ FEED_TOOLS_VERSION = "0.2.18"
36
36
 
37
37
  FEED_TOOLS_NAMESPACES = {
38
38
  "admin" => "http://webns.net/mvcb/",
39
39
  "ag" => "http://purl.org/rss/1.0/modules/aggregation/",
40
40
  "annotate" => "http://purl.org/rss/1.0/modules/annotate/",
41
- "atom" => "http://www.w3.org/2005/Atom",
41
+ "atom10" => "http://www.w3.org/2005/Atom",
42
42
  "atom03" => "http://purl.org/atom/ns#",
43
43
  "audio" => "http://media.tangent.org/rss/1.0/",
44
44
  "blogChannel" => "http://backend.userland.com/blogChannelModule",
@@ -123,6 +123,7 @@ begin
123
123
  require 'cgi'
124
124
  require 'pp'
125
125
  require 'yaml'
126
+ require 'base64'
126
127
 
127
128
  require_gem('activerecord', '>= 1.10.1')
128
129
  require_gem('uuidtools', '>= 0.1.2')
@@ -611,25 +612,42 @@ module FeedTools
611
612
  end
612
613
 
613
614
  # Creates a merged "planet" feed from a set of urls.
614
- def FeedTools.build_merged_feed(url_array)
615
+ #
616
+ # Options are:
617
+ # * <tt>:multi_threaded</tt> - If set to true, feeds will
618
+ # be retrieved concurrently. Not recommended when used
619
+ # in conjunction with the DatabaseFeedCache as it will
620
+ # open multiple connections to the database.
621
+ def FeedTools.build_merged_feed(url_array, options = {})
622
+ validate_options([ :multi_threaded ],
623
+ options.keys)
624
+ options = { :multi_threaded => false }.merge(options)
615
625
  return nil if url_array.nil?
616
626
  merged_feed = FeedTools::Feed.new
617
627
  retrieved_feeds = []
618
- feed_threads = []
619
- url_array.each do |feed_url|
620
- feed_threads << Thread.new do
628
+ if options[:multi_threaded]
629
+ feed_threads = []
630
+ url_array.each do |feed_url|
631
+ feed_threads << Thread.new do
632
+ feed = Feed.open(feed_url)
633
+ retrieved_feeds << feed
634
+ end
635
+ end
636
+ feed_threads.each do |thread|
637
+ thread.join
638
+ end
639
+ else
640
+ url_array.each do |feed_url|
621
641
  feed = Feed.open(feed_url)
622
642
  retrieved_feeds << feed
623
643
  end
624
644
  end
625
- feed_threads.each do |thread|
626
- thread.join
627
- end
628
645
  retrieved_feeds.each do |feed|
629
646
  merged_feed.entries.concat(
630
647
  feed.entries.collect do |entry|
631
- entry.title = "#{feed.title}: #{entry.title}"
632
- entry
648
+ new_entry = entry.dup
649
+ new_entry.title = "#{feed.title}: #{entry.title}"
650
+ new_entry
633
651
  end )
634
652
  end
635
653
  return merged_feed
@@ -642,7 +660,11 @@ module REXML # :nodoc:
642
660
  def inner_xml # :nodoc:
643
661
  result = ""
644
662
  self.each_child do |child|
645
- result << child.to_s
663
+ if child.kind_of? REXML::Comment
664
+ result << "<!--" + child.to_s + "-->"
665
+ else
666
+ result << child.to_s
667
+ end
646
668
  end
647
669
  return result
648
670
  end
@@ -1,3 +1,26 @@
1
+ #--
2
+ # Copyright (c) 2005 Robert Aman
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
1
24
  #= database_feed_cache.rb
2
25
  #
3
26
  # The <tt>DatabaseFeedCache</tt> is the default caching mechanism for
@@ -22,6 +45,7 @@ module FeedTools
22
45
  def DatabaseFeedCache.initialize_cache
23
46
  # Establish a connection if we don't already have one
24
47
  begin
48
+ ActiveRecord::Base.default_timezone = :utc
25
49
  ActiveRecord::Base.connection
26
50
  rescue
27
51
  begin
@@ -1,7 +1,38 @@
1
+ #--
2
+ # Copyright (c) 2005 Robert Aman
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require 'feed_tools/helpers/generic_helper'
25
+
1
26
  module FeedTools
2
27
  # The <tt>FeedTools::Feed</tt> class represents a web feed's structure.
3
- class Feed
4
- include REXML # :nodoc:
28
+ class Feed
29
+ # :stopdoc:
30
+ include REXML
31
+ class << self
32
+ include GenericHelper
33
+ private :validate_options
34
+ end
35
+ # :startdoc:
5
36
 
6
37
  # Represents a feed/feed item's category
7
38
  class Category
@@ -115,17 +146,7 @@ module FeedTools
115
146
  @items = nil
116
147
  @live = false
117
148
  end
118
-
119
- # Raises an exception if an invalid option has been specified to
120
- # prevent misspellings from slipping through
121
- def Feed.validate_options(valid_option_keys, supplied_option_keys)
122
- unknown_option_keys = supplied_option_keys - valid_option_keys
123
- unless unknown_option_keys.empty?
124
- raise ArgumentError, "Unknown options: #{unknown_option_keys}"
125
- end
126
- end
127
- class << self; private :validate_options; end
128
-
149
+
129
150
  # Loads the feed specified by the url, pulling the data from the
130
151
  # cache if it hasn't expired.
131
152
  # Options are:
@@ -319,6 +340,8 @@ module FeedTools
319
340
  end
320
341
  rescue SocketError
321
342
  raise FeedAccessError, 'Socket error prevented feed retrieval'
343
+ rescue Timeout::Error
344
+ raise FeedAccessError, 'Timeout while attempting to retrieve feed'
322
345
  end
323
346
  end
324
347
 
@@ -699,9 +722,16 @@ module FeedTools
699
722
  unless channel_node.nil?
700
723
  @id = XPath.first(channel_node, "id/text()").to_s
701
724
  if @id == ""
702
- @id = XPath.first(channel_node, "atom:id/text()",
725
+ @id = XPath.first(channel_node, "atom10:id/text()",
726
+ FEED_TOOLS_NAMESPACES).to_s
727
+ end
728
+ if @id == ""
729
+ @id = XPath.first(channel_node, "atom03:id/text()",
703
730
  FEED_TOOLS_NAMESPACES).to_s
704
731
  end
732
+ if @id == ""
733
+ @id = XPath.first(channel_node, "atom:id/text()").to_s
734
+ end
705
735
  if @id == ""
706
736
  @id = XPath.first(channel_node, "guid/text()").to_s
707
737
  end
@@ -711,9 +741,16 @@ module FeedTools
711
741
  @id = XPath.first(root_node, "id/text()").to_s
712
742
  end
713
743
  if @id == ""
714
- @id = XPath.first(root_node, "atom:id/text()",
744
+ @id = XPath.first(channel_node, "atom10:id/text()",
745
+ FEED_TOOLS_NAMESPACES).to_s
746
+ end
747
+ if @id == ""
748
+ @id = XPath.first(channel_node, "atom03:id/text()",
715
749
  FEED_TOOLS_NAMESPACES).to_s
716
750
  end
751
+ if @id == ""
752
+ @id = XPath.first(channel_node, "atom:id/text()").to_s
753
+ end
717
754
  if @id == ""
718
755
  @id = XPath.first(root_node, "guid/text()").to_s
719
756
  end
@@ -757,7 +794,7 @@ module FeedTools
757
794
  @url = nil if @url == ""
758
795
  end
759
796
  if override_url.call
760
- @url = XPath.first(channel_node, "atom:link[@rel='self']/@href",
797
+ @url = XPath.first(channel_node, "atom10:link[@rel='self']/@href",
761
798
  FEED_TOOLS_NAMESPACES).to_s
762
799
  @url = nil if @url == ""
763
800
  end
@@ -805,7 +842,22 @@ module FeedTools
805
842
  if @title.nil?
806
843
  unless channel_node.nil?
807
844
  repair_entities = false
808
- title_node = XPath.first(channel_node, "title")
845
+ title_node = XPath.first(channel_node, "atom10:title",
846
+ FEED_TOOLS_NAMESPACES)
847
+ if title_node.nil?
848
+ title_node = XPath.first(channel_node, "title")
849
+ end
850
+ if title_node.nil?
851
+ title_node = XPath.first(channel_node, "atom03:title",
852
+ FEED_TOOLS_NAMESPACES)
853
+ end
854
+ if title_node.nil?
855
+ title_node = XPath.first(channel_node, "atom:title")
856
+ end
857
+ if title_node.nil?
858
+ title_node = XPath.first(channel_node, "dc:title",
859
+ FEED_TOOLS_NAMESPACES)
860
+ end
809
861
  if title_node.nil?
810
862
  title_node = XPath.first(channel_node, "dc:title")
811
863
  end
@@ -816,16 +868,21 @@ module FeedTools
816
868
  if title_node.nil?
817
869
  return nil
818
870
  end
819
- if XPath.first(title_node, "@type").to_s == "xhtml" ||
820
- XPath.first(title_node, "@mode").to_s == "xhtml" ||
821
- XPath.first(title_node, "@type").to_s == "xml" ||
822
- XPath.first(title_node, "@mode").to_s == "xml" ||
823
- XPath.first(title_node, "@type").to_s == "application/xhtml+xml"
871
+ title_type = XPath.first(title_node, "@type").to_s
872
+ title_mode = XPath.first(title_node, "@mode").to_s
873
+ title_encoding = XPath.first(title_node, "@encoding").to_s
874
+
875
+ # Note that we're checking for misuse of type, mode and encoding here
876
+ if title_type == "base64" || title_mode == "base64" ||
877
+ title_encoding == "base64"
878
+ @title = Base64.decode64(title_node.inner_xml.strip)
879
+ elsif title_type == "xhtml" || title_mode == "xhtml" ||
880
+ title_type == "xml" || title_mode == "xml" ||
881
+ title_type == "application/xhtml+xml"
824
882
  @title = title_node.inner_xml
825
- elsif XPath.first(title_node, "@type").to_s == "escaped" ||
826
- XPath.first(title_node, "@mode").to_s == "escaped"
883
+ elsif title_type == "escaped" || title_mode == "escaped"
827
884
  @title = FeedTools.unescape_entities(
828
- XPath.first(title_node, "text()").to_s)
885
+ title_node.inner_xml)
829
886
  else
830
887
  @title = title_node.inner_xml
831
888
  repair_entities = true
@@ -900,27 +957,29 @@ module FeedTools
900
957
  if description_node.nil?
901
958
  return nil
902
959
  end
903
- unless description_node.nil?
904
- if XPath.first(description_node, "@encoding").to_s != ""
905
- @description =
906
- "[Embedded data objects are not currently supported.]"
907
- elsif description_node.cdatas.size > 0
908
- @description = description_node.cdatas.first.value
909
- elsif XPath.first(description_node, "@type").to_s == "xhtml" ||
910
- XPath.first(description_node, "@mode").to_s == "xhtml" ||
911
- XPath.first(description_node, "@type").to_s == "xml" ||
912
- XPath.first(description_node, "@mode").to_s == "xml" ||
913
- XPath.first(description_node, "@type").to_s ==
914
- "application/xhtml+xml"
915
- @description = description_node.inner_xml
916
- elsif XPath.first(description_node, "@type").to_s == "escaped" ||
917
- XPath.first(description_node, "@mode").to_s == "escaped"
918
- @description = FeedTools.unescape_entities(
919
- description_node.inner_xml)
920
- else
921
- @description = description_node.inner_xml
922
- repair_entities = true
923
- end
960
+ description_type = XPath.first(description_node, "@type").to_s
961
+ description_mode = XPath.first(description_node, "@mode").to_s
962
+ description_encoding = XPath.first(description_node, "@encoding").to_s
963
+
964
+ # Note that we're checking for misuse of type, mode and encoding here
965
+ if description_encoding != ""
966
+ @description =
967
+ "[Embedded data objects are not currently supported.]"
968
+ elsif description_node.cdatas.size > 0
969
+ @description = description_node.cdatas.first.value
970
+ elsif description_type == "base64" || description_mode == "base64" ||
971
+ description_encoding == "base64"
972
+ @description = Base64.decode64(description_node.inner_xml.strip)
973
+ elsif description_type == "xhtml" || description_mode == "xhtml" ||
974
+ description_type == "xml" || description_mode == "xml" ||
975
+ description_type == "application/xhtml+xml"
976
+ @description = description_node.inner_xml
977
+ elsif description_type == "escaped" || description_mode == "escaped"
978
+ @description = FeedTools.unescape_entities(
979
+ description_node.inner_xml)
980
+ else
981
+ @description = description_node.inner_xml
982
+ repair_entities = true
924
983
  end
925
984
  if @description == ""
926
985
  @description = self.itunes_summary
@@ -1106,18 +1165,34 @@ module FeedTools
1106
1165
  if @author.nil?
1107
1166
  @author = FeedTools::Feed::Author.new
1108
1167
  unless channel_node.nil?
1109
- author_node = XPath.first(channel_node, "author")
1168
+ author_node = XPath.first(channel_node, "atom10:author",
1169
+ FEED_TOOLS_NAMESPACES)
1170
+ if author_node.nil?
1171
+ author_node = XPath.first(channel_node, "atom03:author",
1172
+ FEED_TOOLS_NAMESPACES)
1173
+ end
1174
+ if author_node.nil?
1175
+ author_node = XPath.first(channel_node, "atom:author")
1176
+ end
1177
+ if author_node.nil?
1178
+ author_node = XPath.first(channel_node, "author")
1179
+ end
1110
1180
  if author_node.nil?
1111
1181
  author_node = XPath.first(channel_node, "managingEditor")
1112
1182
  end
1183
+ if author_node.nil?
1184
+ author_node = XPath.first(channel_node, "dc:author",
1185
+ FEED_TOOLS_NAMESPACES)
1186
+ end
1113
1187
  if author_node.nil?
1114
1188
  author_node = XPath.first(channel_node, "dc:author")
1115
1189
  end
1116
1190
  if author_node.nil?
1117
- author_node = XPath.first(channel_node, "dc:creator")
1191
+ author_node = XPath.first(channel_node, "dc:creator",
1192
+ FEED_TOOLS_NAMESPACES)
1118
1193
  end
1119
1194
  if author_node.nil?
1120
- author_node = XPath.first(channel_node, "atom:author")
1195
+ author_node = XPath.first(channel_node, "dc:creator")
1121
1196
  end
1122
1197
  end
1123
1198
  unless author_node.nil?
@@ -1316,7 +1391,7 @@ module FeedTools
1316
1391
  end
1317
1392
  begin
1318
1393
  if time_string != nil && time_string != ""
1319
- @time = Time.parse(time_string)
1394
+ @time = Time.parse(time_string).gmtime
1320
1395
  else
1321
1396
  @time = Time.now.gmtime
1322
1397
  end
@@ -1342,7 +1417,7 @@ module FeedTools
1342
1417
  end
1343
1418
  end
1344
1419
  if updated_string != nil && updated_string != ""
1345
- @updated = Time.parse(updated_string) rescue nil
1420
+ @updated = Time.parse(updated_string).gmtime rescue nil
1346
1421
  else
1347
1422
  @updated = nil
1348
1423
  end
@@ -1371,7 +1446,7 @@ module FeedTools
1371
1446
  end
1372
1447
  end
1373
1448
  if issued_string != nil && issued_string != ""
1374
- @issued = Time.parse(issued_string) rescue nil
1449
+ @issued = Time.parse(issued_string).gmtime rescue nil
1375
1450
  else
1376
1451
  @issued = nil
1377
1452
  end
@@ -1400,7 +1475,7 @@ module FeedTools
1400
1475
  end
1401
1476
  end
1402
1477
  if published_string != nil && published_string != ""
1403
- @published = Time.parse(published_string) rescue nil
1478
+ @published = Time.parse(published_string).gmtime rescue nil
1404
1479
  else
1405
1480
  @published = nil
1406
1481
  end
@@ -1531,20 +1606,70 @@ module FeedTools
1531
1606
  # Returns the feed's copyright information
1532
1607
  def copyright
1533
1608
  if @copyright.nil?
1534
- unless channel_node.nil?
1535
- @copyright = XPath.first(channel_node, "copyright/text()").to_s
1536
- if @copyright == ""
1537
- @copyright = XPath.first(channel_node, "rights/text()").to_s
1609
+ unless root_node.nil?
1610
+ repair_entities = false
1611
+ copyright_node = XPath.first(channel_node, "dc:rights")
1612
+ if copyright_node.nil?
1613
+ copyright_node = XPath.first(channel_node, "dc:rights",
1614
+ FEED_TOOLS_NAMESPACES)
1615
+ end
1616
+ if copyright_node.nil?
1617
+ copyright_node = XPath.first(channel_node, "rights",
1618
+ FEED_TOOLS_NAMESPACES)
1538
1619
  end
1539
- if @copyright == ""
1540
- @copyright = XPath.first(channel_node, "dc:rights/text()").to_s
1620
+ if copyright_node.nil?
1621
+ copyright_node = XPath.first(channel_node, "copyright",
1622
+ FEED_TOOLS_NAMESPACES)
1541
1623
  end
1542
- if @copyright == ""
1543
- @copyright = XPath.first(channel_node, "copyrights/text()").to_s
1624
+ if copyright_node.nil?
1625
+ copyright_node = XPath.first(channel_node, "atom03:copyright",
1626
+ FEED_TOOLS_NAMESPACES)
1544
1627
  end
1628
+ if copyright_node.nil?
1629
+ copyright_node = XPath.first(channel_node, "atom10:copyright",
1630
+ FEED_TOOLS_NAMESPACES)
1631
+ end
1632
+ if copyright_node.nil?
1633
+ copyright_node = XPath.first(channel_node, "copyrights",
1634
+ FEED_TOOLS_NAMESPACES)
1635
+ end
1636
+ end
1637
+ if copyright_node.nil?
1638
+ return nil
1639
+ end
1640
+ copyright_type = XPath.first(copyright_node, "@type").to_s
1641
+ copyright_mode = XPath.first(copyright_node, "@mode").to_s
1642
+ copyright_encoding = XPath.first(copyright_node, "@encoding").to_s
1643
+
1644
+ # Note that we're checking for misuse of type, mode and encoding here
1645
+ if copyright_encoding != ""
1646
+ @copyright =
1647
+ "[Embedded data objects are not currently supported.]"
1648
+ elsif copyright_node.cdatas.size > 0
1649
+ @copyright = copyright_node.cdatas.first.value
1650
+ elsif copyright_type == "base64" || copyright_mode == "base64" ||
1651
+ copyright_encoding == "base64"
1652
+ @copyright = Base64.decode64(copyright_node.inner_xml.strip)
1653
+ elsif copyright_type == "xhtml" || copyright_mode == "xhtml" ||
1654
+ copyright_type == "xml" || copyright_mode == "xml" ||
1655
+ copyright_type == "application/xhtml+xml"
1656
+ @copyright = copyright_node.inner_xml
1657
+ elsif copyright_type == "escaped" || copyright_mode == "escaped"
1658
+ @copyright = FeedTools.unescape_entities(
1659
+ copyright_node.inner_xml)
1660
+ else
1661
+ @copyright = copyright_node.inner_xml
1662
+ repair_entities = true
1663
+ end
1664
+
1665
+ unless @copyright.nil?
1545
1666
  @copyright = FeedTools.sanitize_html(@copyright, :strip)
1546
- @copyright = nil if @copyright == ""
1667
+ @copyright = FeedTools.unescape_entities(@copyright) if repair_entities
1668
+ @copyright = FeedTools.tidy_html(@copyright)
1547
1669
  end
1670
+
1671
+ @copyright = @copyright.strip unless @copyright.nil?
1672
+ @copyright = nil if @copyright == ""
1548
1673
  end
1549
1674
  return @copyright
1550
1675
  end
@@ -1596,15 +1721,16 @@ module FeedTools
1596
1721
  @time_to_live = update_frequency.to_i.year
1597
1722
  elsif update_frequency.to_i >= 3000
1598
1723
  # Normally, this should default to minutes, but realistically,
1599
- # if they meant minutes, you're rarely going to see a value higher
1600
- # than 120. If we see >= 3000, we're either dealing with a stupid
1601
- # pseudo-spec that decided to use seconds, or we're looking at
1602
- # someone who only has weekly updated content. Worst case, we
1603
- # misreport the time, and we update too often. Best case, we
1604
- # avoid accidentally updating the feed only once a year. In the
1605
- # interests of being pragmatic, and since the problem we avoid
1606
- # is a far greater one than the one we cause, just run the check
1607
- # and hope no one actually gets hurt.
1724
+ # if they meant minutes, you're rarely going to see a value
1725
+ # higher than 120. If we see >= 3000, we're either dealing
1726
+ # with a stupid pseudo-spec that decided to use seconds, or
1727
+ # we're looking at someone who only has weekly updated
1728
+ # content. Worst case, we misreport the time, and we update
1729
+ # too often. Best case, we avoid accidentally updating the
1730
+ # feed only once a year. In the interests of being pragmatic,
1731
+ # and since the problem we avoid is a far greater one than
1732
+ # the one we cause, just run the check and hope no one
1733
+ # actually gets hurt.
1608
1734
  @time_to_live = update_frequency.to_i
1609
1735
  else
1610
1736
  @time_to_live = update_frequency.to_i.minute
@@ -1628,7 +1754,8 @@ module FeedTools
1628
1754
  @time_to_live = @time_to_live + update_frequency_hours.to_i.hour
1629
1755
  end
1630
1756
  if update_frequency_minutes != ""
1631
- @time_to_live = @time_to_live + update_frequency_minutes.to_i.minute
1757
+ @time_to_live = @time_to_live +
1758
+ update_frequency_minutes.to_i.minute
1632
1759
  end
1633
1760
  if update_frequency_seconds != ""
1634
1761
  @time_to_live = @time_to_live + update_frequency_seconds.to_i
@@ -1790,7 +1917,6 @@ module FeedTools
1790
1917
  new_item = FeedItem.new
1791
1918
  new_item.feed_data = item_node.to_s
1792
1919
  new_item.feed_data_type = self.feed_data_type
1793
- new_item.feed = self
1794
1920
  @items << new_item
1795
1921
  end
1796
1922
  end
@@ -1802,9 +1928,30 @@ module FeedTools
1802
1928
  end
1803
1929
  return @items
1804
1930
  end
1931
+
1932
+ # Sets the items array to a new array.
1933
+ def items=(new_items)
1934
+ for item in new_items
1935
+ unless item.kind_of? FeedTools::FeedItem
1936
+ raise ArgumentError,
1937
+ "You should only add FeedItem objects to the items array."
1938
+ end
1939
+ end
1940
+ @items = new_items
1941
+ end
1942
+
1943
+ # Syntactic sugar for appending feed items to a feed.
1944
+ def <<(new_item)
1945
+ @items ||= []
1946
+ unless new_item.kind_of? FeedTools::FeedItem
1947
+ raise ArgumentError,
1948
+ "You should only add FeedItem objects to the items array."
1949
+ end
1950
+ @items << new_item
1951
+ end
1805
1952
 
1806
- # The time that the feed was last requested from the remote server. Nil if it has
1807
- # never been pulled, or if it was created from scratch.
1953
+ # The time that the feed was last requested from the remote server. Nil
1954
+ # if it has never been pulled, or if it was created from scratch.
1808
1955
  def last_retrieved
1809
1956
  unless self.cache_object.nil?
1810
1957
  @last_retrieved = self.cache_object.last_retrieved
@@ -1850,8 +1997,13 @@ module FeedTools
1850
1997
  # True if the feed has expired and must be reacquired from the remote
1851
1998
  # server.
1852
1999
  def expired?
1853
- return self.last_retrieved == nil ||
1854
- (self.last_retrieved + self.time_to_live) < Time.now.gmtime
2000
+ if (self.last_retrieved == nil)
2001
+ return true
2002
+ elsif (self.time_to_live < 30.minutes)
2003
+ return (self.last_retrieved + 30.minutes) < Time.now.gmtime
2004
+ else
2005
+ return (self.last_retrieved + self.time_to_live) < Time.now.gmtime
2006
+ end
1855
2007
  end
1856
2008
 
1857
2009
  # Forces this feed to expire.
@@ -1914,16 +2066,19 @@ module FeedTools
1914
2066
  xml_builder.tag!("dc:language", language)
1915
2067
  end
1916
2068
  xml_builder.tag!("syn:updatePeriod", "hourly")
1917
- xml_builder.tag!("syn:updateFrequency", (time_to_live / 1.hour).to_s)
2069
+ xml_builder.tag!("syn:updateFrequency",
2070
+ (time_to_live / 1.hour).to_s)
1918
2071
  xml_builder.tag!("syn:updateBase", Time.mktime(1970).iso8601)
1919
2072
  xml_builder.items do
1920
2073
  xml_builder.tag!("rdf:Seq") do
1921
2074
  unless items.nil?
1922
2075
  for item in items
1923
2076
  if item.link.nil?
1924
- raise "Cannot generate an rdf-based feed with a nil item link field."
2077
+ raise "Cannot generate an rdf-based feed with a nil " +
2078
+ "item link field."
1925
2079
  end
1926
- xml_builder.tag!("rdf:li", "rdf:resource" => CGI.escapeHTML(item.link))
2080
+ xml_builder.tag!("rdf:li", "rdf:resource" =>
2081
+ CGI.escapeHTML(item.link))
1927
2082
  end
1928
2083
  end
1929
2084
  end
@@ -1939,7 +2094,8 @@ module FeedTools
1939
2094
  end
1940
2095
  end
1941
2096
  best_image = images.first if best_image.nil?
1942
- xml_builder.image("rdf:about" => CGI.escapeHTML(best_image.url)) do
2097
+ xml_builder.image(
2098
+ "rdf:about" => CGI.escapeHTML(best_image.url)) do
1943
2099
  if best_image.title != nil && best_image.title != ""
1944
2100
  xml_builder.title(best_image.title)
1945
2101
  elsif self.title != nil && self.title != ""
@@ -2040,7 +2196,7 @@ module FeedTools
2040
2196
  end
2041
2197
  elsif feed_type == "atom" && version == 1.0
2042
2198
  # normal atom format
2043
- return xml_builder.feed("xmlns" => FEED_TOOLS_NAMESPACES['atom'],
2199
+ return xml_builder.feed("xmlns" => FEED_TOOLS_NAMESPACES['atom10'],
2044
2200
  "xml:lang" => language) do
2045
2201
  unless title.nil? || title == ""
2046
2202
  xml_builder.title(title,