feedtools 0.2.17 → 0.2.18

Sign up to get free protection for your applications and to get access to all the features.
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,