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