1require 'rss/parser'
2
3module RSS
4  ##
5  # Atom is an XML-based document format that is used to describe 'feeds' of related information.
6  # A typical use is in a news feed where the information is periodically updated and which users
7  # can subscribe to.  The Atom format is described in http://tools.ietf.org/html/rfc4287
8  #
9  # The Atom module provides support in reading and creating feeds.
10  #
11  # See the RSS module for examples consuming and creating feeds
12  module Atom
13
14    ##
15    # The Atom URI W3C Namespace
16
17    URI = "http://www.w3.org/2005/Atom"
18
19    ##
20    # The XHTML URI W3C Namespace
21
22    XHTML_URI = "http://www.w3.org/1999/xhtml"
23
24    module CommonModel
25      NSPOOL = {}
26      ELEMENTS = []
27
28      def self.append_features(klass)
29        super
30        klass.install_must_call_validator("atom", URI)
31        [
32         ["lang", :xml],
33         ["base", :xml],
34        ].each do |name, uri, required|
35          klass.install_get_attribute(name, uri, required, [nil, :inherit])
36        end
37        klass.class_eval do
38          class << self
39            def required_uri
40              URI
41            end
42
43            def need_parent?
44              true
45            end
46          end
47        end
48      end
49    end
50
51    module ContentModel
52      module ClassMethods
53        def content_type
54          @content_type ||= nil
55        end
56      end
57
58      class << self
59        def append_features(klass)
60          super
61          klass.extend(ClassMethods)
62          klass.content_setup(klass.content_type, klass.tag_name)
63        end
64      end
65
66      def maker_target(target)
67        target
68      end
69
70      private
71      def setup_maker_element_writer
72        "#{self.class.name.split(/::/).last.downcase}="
73      end
74
75      def setup_maker_element(target)
76        target.__send__(setup_maker_element_writer, content)
77        super
78      end
79    end
80
81    module URIContentModel
82      class  << self
83        def append_features(klass)
84          super
85          klass.class_eval do
86            @content_type = [nil, :uri]
87            include(ContentModel)
88          end
89        end
90      end
91    end
92
93    # The TextConstruct module is used to define a Text construct Atom element,
94    # which is used to store small quantities of human-readable text
95    #
96    # The TextConstruct has a type attribute, e.g. text, html, xhtml
97    module TextConstruct
98      def self.append_features(klass)
99        super
100        klass.class_eval do
101          [
102           ["type", ""],
103          ].each do |name, uri, required|
104            install_get_attribute(name, uri, required, :text_type)
105          end
106
107          content_setup
108          add_need_initialize_variable("xhtml")
109
110          class << self
111            def xml_getter
112              "xhtml"
113            end
114
115            def xml_setter
116              "xhtml="
117            end
118          end
119        end
120      end
121
122      attr_writer :xhtml
123
124      def xhtml
125        return @xhtml if @xhtml.nil?
126        if @xhtml.is_a?(XML::Element) and
127            [@xhtml.name, @xhtml.uri] == ["div", XHTML_URI]
128          return @xhtml
129        end
130
131        children = @xhtml
132        children = [children] unless children.is_a?(Array)
133        XML::Element.new("div", nil, XHTML_URI,
134                         {"xmlns" => XHTML_URI}, children)
135      end
136
137      # Returns true if type is "xhtml"
138      def have_xml_content?
139        @type == "xhtml"
140      end
141
142      def atom_validate(ignore_unknown_element, tags, uri)
143        if have_xml_content?
144          if @xhtml.nil?
145            raise MissingTagError.new("div", tag_name)
146          end
147          unless [@xhtml.name, @xhtml.uri] == ["div", XHTML_URI]
148            raise NotExpectedTagError.new(@xhtml.name, @xhtml.uri, tag_name)
149          end
150        end
151      end
152
153      private
154      def maker_target(target)
155        target.__send__(self.class.name.split(/::/).last.downcase) {|x| x}
156      end
157
158      def setup_maker_attributes(target)
159        target.type = type
160        target.content = content
161        target.xml_content = @xhtml
162      end
163    end
164
165    # The PersonConstruct module is used to define a Person Atom element that can be
166    # used to describe a person, corporation, or similar entity
167    #
168    # The PersonConstruct has a Name, Uri, and Email child elements
169    module PersonConstruct
170
171      # Adds attributes for name, uri, and email to the +klass+
172      def self.append_features(klass)
173        super
174        klass.class_eval do
175          [
176           ["name", nil],
177           ["uri", "?"],
178           ["email", "?"],
179          ].each do |tag, occurs|
180            install_have_attribute_element(tag, URI, occurs, nil, :content)
181          end
182        end
183      end
184
185      def maker_target(target)
186        target.__send__("new_#{self.class.name.split(/::/).last.downcase}")
187      end
188
189      # The name of the person or entity
190      class Name < RSS::Element
191        include CommonModel
192        include ContentModel
193      end
194
195      # The URI of the person or entity
196      class Uri < RSS::Element
197        include CommonModel
198        include URIContentModel
199      end
200
201      # The email of the person or entity
202      class Email < RSS::Element
203        include CommonModel
204        include ContentModel
205      end
206    end
207
208    # Element used to describe an Atom date and time in the ISO 8601 format
209    #
210    # Examples:
211    # * 2013-03-04T15:30:02Z
212    # * 2013-03-04T10:30:02-05:00
213    module DateConstruct
214      def self.append_features(klass)
215        super
216        klass.class_eval do
217          @content_type = :w3cdtf
218          include(ContentModel)
219        end
220      end
221
222      # Raises NotAvailableValueError if element content is nil
223      def atom_validate(ignore_unknown_element, tags, uri)
224        raise NotAvailableValueError.new(tag_name, "") if content.nil?
225      end
226    end
227
228    module DuplicateLinkChecker
229      # Checks if there are duplicate links with the same type and hreflang attributes
230      # that have an alternate (or empty) rel attribute
231      #
232      # Raises a TooMuchTagError if there are duplicates found
233      def validate_duplicate_links(links)
234        link_infos = {}
235        links.each do |link|
236          rel = link.rel || "alternate"
237          next unless rel == "alternate"
238          key = [link.hreflang, link.type]
239          if link_infos.has_key?(key)
240            raise TooMuchTagError.new("link", tag_name)
241          end
242          link_infos[key] = true
243        end
244      end
245    end
246
247    # Atom feed element
248    #
249    # A Feed has several metadata attributes in addition to a number of Entry child elements
250    class Feed < RSS::Element
251      include RootElementMixin
252      include CommonModel
253      include DuplicateLinkChecker
254
255      install_ns('', URI)
256
257      [
258       ["author", "*", :children],
259       ["category", "*", :children, "categories"],
260       ["contributor", "*", :children],
261       ["generator", "?"],
262       ["icon", "?", nil, :content],
263       ["id", nil, nil, :content],
264       ["link", "*", :children],
265       ["logo", "?"],
266       ["rights", "?"],
267       ["subtitle", "?", nil, :content],
268       ["title", nil, nil, :content],
269       ["updated", nil, nil, :content],
270       ["entry", "*", :children, "entries"],
271      ].each do |tag, occurs, type, *args|
272        type ||= :child
273        __send__("install_have_#{type}_element",
274                 tag, URI, occurs, tag, *args)
275      end
276
277      # Creates a new Atom feed
278      def initialize(version=nil, encoding=nil, standalone=nil)
279        super("1.0", version, encoding, standalone)
280        @feed_type = "atom"
281        @feed_subtype = "feed"
282      end
283
284      alias_method :items, :entries
285
286      # Returns true if there are any authors for the feed or any of the Entry
287      # child elements have an author
288      def have_author?
289        authors.any? {|author| !author.to_s.empty?} or
290          entries.any? {|entry| entry.have_author?(false)}
291      end
292
293      private
294      def atom_validate(ignore_unknown_element, tags, uri)
295        unless have_author?
296          raise MissingTagError.new("author", tag_name)
297        end
298        validate_duplicate_links(links)
299      end
300
301      def have_required_elements?
302        super and have_author?
303      end
304
305      def maker_target(maker)
306        maker.channel
307      end
308
309      def setup_maker_element(channel)
310        prev_dc_dates = channel.dc_dates.to_a.dup
311        super
312        channel.about = id.content if id
313        channel.dc_dates.replace(prev_dc_dates)
314      end
315
316      def setup_maker_elements(channel)
317        super
318        items = channel.maker.items
319        entries.each do |entry|
320          entry.setup_maker(items)
321        end
322      end
323
324      class Author < RSS::Element
325        include CommonModel
326        include PersonConstruct
327      end
328
329      class Category < RSS::Element
330        include CommonModel
331
332        [
333         ["term", "", true],
334         ["scheme", "", false, [nil, :uri]],
335         ["label", ""],
336        ].each do |name, uri, required, type|
337          install_get_attribute(name, uri, required, type)
338        end
339
340        private
341        def maker_target(target)
342          target.new_category
343        end
344      end
345
346      class Contributor < RSS::Element
347        include CommonModel
348        include PersonConstruct
349      end
350
351      class Generator < RSS::Element
352        include CommonModel
353        include ContentModel
354
355        [
356         ["uri", "", false, [nil, :uri]],
357         ["version", ""],
358        ].each do |name, uri, required, type|
359          install_get_attribute(name, uri, required, type)
360        end
361
362        private
363        def setup_maker_attributes(target)
364          target.generator do |generator|
365            generator.uri = uri if uri
366            generator.version = version if version
367          end
368        end
369      end
370
371      # Atom Icon element
372      #
373      # Image that provides a visual identification for the Feed.  Image should have an aspect
374      # ratio of 1:1
375      class Icon < RSS::Element
376        include CommonModel
377        include URIContentModel
378      end
379
380      # Atom ID element
381      #
382      # Universally Unique Identifier (UUID) for the Feed
383      class Id < RSS::Element
384        include CommonModel
385        include URIContentModel
386      end
387
388      # Defines an Atom Link element
389      #
390      # A Link has the following attributes:
391      # * href
392      # * rel
393      # * type
394      # * hreflang
395      # * title
396      # * length
397      class Link < RSS::Element
398        include CommonModel
399
400        [
401         ["href", "", true, [nil, :uri]],
402         ["rel", ""],
403         ["type", ""],
404         ["hreflang", ""],
405         ["title", ""],
406         ["length", ""],
407        ].each do |name, uri, required, type|
408          install_get_attribute(name, uri, required, type)
409        end
410
411        private
412        def maker_target(target)
413          target.new_link
414        end
415      end
416
417      # Atom Logo element
418      #
419      # Image that provides a visual identification for the Feed.  Image should have an aspect
420      # ratio of 2:1 (horizontal:vertical)
421      class Logo < RSS::Element
422        include CommonModel
423        include URIContentModel
424
425        def maker_target(target)
426          target.maker.image
427        end
428
429        private
430        def setup_maker_element_writer
431          "url="
432        end
433      end
434
435      # Atom Rights element
436      #
437      # TextConstruct that contains copyright information regarding the content in an Entry or Feed
438      class Rights < RSS::Element
439        include CommonModel
440        include TextConstruct
441      end
442
443      # Atom Subtitle element
444      #
445      # TextConstruct that conveys a description or subtitle for a Feed
446      class Subtitle < RSS::Element
447        include CommonModel
448        include TextConstruct
449      end
450
451      # Atom Title element
452      #
453      # TextConstruct that conveys a description or title for a feed or Entry
454      class Title < RSS::Element
455        include CommonModel
456        include TextConstruct
457      end
458
459      # Atom Updated element
460      #
461      # DateConstruct indicating the most recent time when an Entry or Feed was modified
462      # in a way the publisher considers significant
463      class Updated < RSS::Element
464        include CommonModel
465        include DateConstruct
466      end
467
468      # Defines a child Atom Entry element for an Atom Feed
469      class Entry < RSS::Element
470        include CommonModel
471        include DuplicateLinkChecker
472
473        [
474         ["author", "*", :children],
475         ["category", "*", :children, "categories"],
476         ["content", "?", :child],
477         ["contributor", "*", :children],
478         ["id", nil, nil, :content],
479         ["link", "*", :children],
480         ["published", "?", :child, :content],
481         ["rights", "?", :child],
482         ["source", "?"],
483         ["summary", "?", :child],
484         ["title", nil],
485         ["updated", nil, :child, :content],
486        ].each do |tag, occurs, type, *args|
487          type ||= :attribute
488          __send__("install_have_#{type}_element",
489                   tag, URI, occurs, tag, *args)
490        end
491
492        # Returns whether any of the following are true
493        # * There are any authors in the feed
494        # * If the parent element has an author and the +check_parent+ parameter was given.
495        # * There is a source element that has an author
496        def have_author?(check_parent=true)
497          authors.any? {|author| !author.to_s.empty?} or
498            (check_parent and @parent and @parent.have_author?) or
499            (source and source.have_author?)
500        end
501
502        private
503        def atom_validate(ignore_unknown_element, tags, uri)
504          unless have_author?
505            raise MissingTagError.new("author", tag_name)
506          end
507          validate_duplicate_links(links)
508        end
509
510        def have_required_elements?
511          super and have_author?
512        end
513
514        def maker_target(items)
515          if items.respond_to?("items")
516            # For backward compatibility
517            items = items.items
518          end
519          items.new_item
520        end
521
522        Author = Feed::Author
523        Category = Feed::Category
524
525        class Content < RSS::Element
526          include CommonModel
527
528          class << self
529            def xml_setter
530              "xml="
531            end
532
533            def xml_getter
534              "xml"
535            end
536          end
537
538          [
539           ["type", ""],
540           ["src", "", false, [nil, :uri]],
541          ].each do |name, uri, required, type|
542            install_get_attribute(name, uri, required, type)
543          end
544
545          content_setup
546          add_need_initialize_variable("xml")
547
548          attr_writer :xml
549          def have_xml_content?
550            inline_xhtml? or inline_other_xml?
551          end
552
553          def xml
554            return @xml unless inline_xhtml?
555            return @xml if @xml.nil?
556            if @xml.is_a?(XML::Element) and
557                [@xml.name, @xml.uri] == ["div", XHTML_URI]
558              return @xml
559            end
560
561            children = @xml
562            children = [children] unless children.is_a?(Array)
563            XML::Element.new("div", nil, XHTML_URI,
564                             {"xmlns" => XHTML_URI}, children)
565          end
566
567          def xhtml
568            if inline_xhtml?
569              xml
570            else
571              nil
572            end
573          end
574
575          def atom_validate(ignore_unknown_element, tags, uri)
576            if out_of_line?
577              raise MissingAttributeError.new(tag_name, "type") if @type.nil?
578              unless (content.nil? or content.empty?)
579                raise NotAvailableValueError.new(tag_name, content)
580              end
581            elsif inline_xhtml?
582              if @xml.nil?
583                raise MissingTagError.new("div", tag_name)
584              end
585              unless @xml.name == "div" and @xml.uri == XHTML_URI
586                raise NotExpectedTagError.new(@xml.name, @xml.uri, tag_name)
587              end
588            end
589          end
590
591          def inline_text?
592            !out_of_line? and [nil, "text", "html"].include?(@type)
593          end
594
595          def inline_html?
596            return false if out_of_line?
597            @type == "html" or mime_split == ["text", "html"]
598          end
599
600          def inline_xhtml?
601            !out_of_line? and @type == "xhtml"
602          end
603
604          def inline_other?
605            return false if out_of_line?
606            media_type, subtype = mime_split
607            return false if media_type.nil? or subtype.nil?
608            true
609          end
610
611          def inline_other_text?
612            return false unless inline_other?
613            return false if inline_other_xml?
614
615            media_type, = mime_split
616            return true if "text" == media_type.downcase
617            false
618          end
619
620          def inline_other_xml?
621            return false unless inline_other?
622
623            media_type, subtype = mime_split
624            normalized_mime_type = "#{media_type}/#{subtype}".downcase
625            if /(?:\+xml|^xml)$/ =~ subtype or
626                %w(text/xml-external-parsed-entity
627                   application/xml-external-parsed-entity
628                   application/xml-dtd).find {|x| x == normalized_mime_type}
629              return true
630            end
631            false
632          end
633
634          def inline_other_base64?
635            inline_other? and !inline_other_text? and !inline_other_xml?
636          end
637
638          def out_of_line?
639            not @src.nil?
640          end
641
642          def mime_split
643            media_type = subtype = nil
644            if /\A\s*([a-z]+)\/([a-z\+]+)\s*(?:;.*)?\z/i =~ @type.to_s
645              media_type = $1.downcase
646              subtype = $2.downcase
647            end
648            [media_type, subtype]
649          end
650
651          def need_base64_encode?
652            inline_other_base64?
653          end
654
655          private
656          def empty_content?
657            out_of_line? or super
658          end
659        end
660
661        Contributor = Feed::Contributor
662        Id = Feed::Id
663        Link = Feed::Link
664
665        class Published < RSS::Element
666          include CommonModel
667          include DateConstruct
668        end
669
670        Rights = Feed::Rights
671
672        class Source < RSS::Element
673          include CommonModel
674
675          [
676           ["author", "*", :children],
677           ["category", "*", :children, "categories"],
678           ["contributor", "*", :children],
679           ["generator", "?"],
680           ["icon", "?"],
681           ["id", "?", nil, :content],
682           ["link", "*", :children],
683           ["logo", "?"],
684           ["rights", "?"],
685           ["subtitle", "?"],
686           ["title", "?"],
687           ["updated", "?", nil, :content],
688          ].each do |tag, occurs, type, *args|
689            type ||= :attribute
690            __send__("install_have_#{type}_element",
691                     tag, URI, occurs, tag, *args)
692          end
693
694          def have_author?
695            !author.to_s.empty?
696          end
697
698          Author = Feed::Author
699          Category = Feed::Category
700          Contributor = Feed::Contributor
701          Generator = Feed::Generator
702          Icon = Feed::Icon
703          Id = Feed::Id
704          Link = Feed::Link
705          Logo = Feed::Logo
706          Rights = Feed::Rights
707          Subtitle = Feed::Subtitle
708          Title = Feed::Title
709          Updated = Feed::Updated
710        end
711
712        class Summary < RSS::Element
713          include CommonModel
714          include TextConstruct
715        end
716
717        Title = Feed::Title
718        Updated = Feed::Updated
719      end
720    end
721
722    # Defines a top-level Atom Entry element
723    #
724    class Entry < RSS::Element
725      include RootElementMixin
726      include CommonModel
727      include DuplicateLinkChecker
728
729      [
730       ["author", "*", :children],
731       ["category", "*", :children, "categories"],
732       ["content", "?"],
733       ["contributor", "*", :children],
734       ["id", nil, nil, :content],
735       ["link", "*", :children],
736       ["published", "?", :child, :content],
737       ["rights", "?"],
738       ["source", "?"],
739       ["summary", "?"],
740       ["title", nil],
741       ["updated", nil, nil, :content],
742      ].each do |tag, occurs, type, *args|
743        type ||= :attribute
744        __send__("install_have_#{type}_element",
745                 tag, URI, occurs, tag, *args)
746      end
747
748      # Creates a new Atom Entry element
749      def initialize(version=nil, encoding=nil, standalone=nil)
750        super("1.0", version, encoding, standalone)
751        @feed_type = "atom"
752        @feed_subtype = "entry"
753      end
754
755      # Returns the Entry in an array
756      def items
757        [self]
758      end
759
760      # sets up the +maker+ for constructing Entry elements
761      def setup_maker(maker)
762        maker = maker.maker if maker.respond_to?("maker")
763        super(maker)
764      end
765
766      # Returns where there are any authors present or there is a source with an author
767      def have_author?
768        authors.any? {|author| !author.to_s.empty?} or
769          (source and source.have_author?)
770      end
771
772      private
773      def atom_validate(ignore_unknown_element, tags, uri)
774        unless have_author?
775          raise MissingTagError.new("author", tag_name)
776        end
777        validate_duplicate_links(links)
778      end
779
780      def have_required_elements?
781        super and have_author?
782      end
783
784      def maker_target(maker)
785        maker.items.new_item
786      end
787
788      Author = Feed::Entry::Author
789      Category = Feed::Entry::Category
790      Content = Feed::Entry::Content
791      Contributor = Feed::Entry::Contributor
792      Id = Feed::Entry::Id
793      Link = Feed::Entry::Link
794      Published = Feed::Entry::Published
795      Rights = Feed::Entry::Rights
796      Source = Feed::Entry::Source
797      Summary = Feed::Entry::Summary
798      Title = Feed::Entry::Title
799      Updated = Feed::Entry::Updated
800    end
801  end
802
803  Atom::CommonModel::ELEMENTS.each do |name|
804    BaseListener.install_get_text_element(Atom::URI, name, "#{name}=")
805  end
806
807  module ListenerMixin
808    private
809    def initial_start_feed(tag_name, prefix, attrs, ns)
810      check_ns(tag_name, prefix, ns, Atom::URI, false)
811
812      @rss = Atom::Feed.new(@version, @encoding, @standalone)
813      @rss.do_validate = @do_validate
814      @rss.xml_stylesheets = @xml_stylesheets
815      @rss.lang = attrs["xml:lang"]
816      @rss.base = attrs["xml:base"]
817      @last_element = @rss
818      pr = Proc.new do |text, tags|
819        @rss.validate_for_stream(tags) if @do_validate
820      end
821      @proc_stack.push(pr)
822    end
823
824    def initial_start_entry(tag_name, prefix, attrs, ns)
825      check_ns(tag_name, prefix, ns, Atom::URI, false)
826
827      @rss = Atom::Entry.new(@version, @encoding, @standalone)
828      @rss.do_validate = @do_validate
829      @rss.xml_stylesheets = @xml_stylesheets
830      @rss.lang = attrs["xml:lang"]
831      @rss.base = attrs["xml:base"]
832      @last_element = @rss
833      pr = Proc.new do |text, tags|
834        @rss.validate_for_stream(tags) if @do_validate
835      end
836      @proc_stack.push(pr)
837    end
838  end
839end
840