home *** CD-ROM | disk | FTP | other *** search
/ PC go! 2008 October / PCgo 2008-10 (DVD).iso / interface / contents / vollversionen_6617 / 21733 / files / xulrunner / components / FeedProcessor.js < prev    next >
Encoding:
JavaScript  |  2008-08-20  |  64.7 KB  |  1,951 lines

  1. /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
  2. /* ***** BEGIN LICENSE BLOCK *****
  3.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  4.  *
  5.  * The contents of this file are subject to the Mozilla Public License Version
  6.  * 1.1 (the "License"); you may not use this file except in compliance with
  7.  * the License. You may obtain a copy of the License at
  8.  * http://www.mozilla.org/MPL/
  9.  *
  10.  * Software distributed under the License is distributed on an "AS IS" basis,
  11.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  12.  * for the specific language governing rights and limitations under the
  13.  * License.
  14.  *
  15.  * The Original Code is mozilla.org code.
  16.  *
  17.  * The Initial Developer of the Original Code is Robert Sayre.
  18.  * Portions created by the Initial Developer are Copyright (C) 2006
  19.  * the Initial Developer. All Rights Reserved.
  20.  *
  21.  * Contributor(s):
  22.  *   Ben Goodger <beng@google.com>
  23.  *   Myk Melez <myk@mozilla.org>
  24.  *   Michael Ventnor <m.ventnor@gmail.com>
  25.  *   Will Guaraldi <will.guaraldi@pculture.org>
  26.  *
  27.  * Alternatively, the contents of this file may be used under the terms of
  28.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  29.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  30.  * in which case the provisions of the GPL or the LGPL are applicable instead
  31.  * of those above. If you wish to allow use of your version of this file only
  32.  * under the terms of either the GPL or the LGPL, and not to allow others to
  33.  * use your version of this file under the terms of the MPL, indicate your
  34.  * decision by deleting the provisions above and replace them with the notice
  35.  * and other provisions required by the GPL or the LGPL. If you do not delete
  36.  * the provisions above, a recipient may use your version of this file under
  37.  * the terms of any one of the MPL, the GPL or the LGPL.
  38.  *
  39.  * ***** END LICENSE BLOCK ***** */
  40.  
  41. function LOG(str) {
  42.   dump("*** " + str + "\n");
  43. }
  44.  
  45. const Ci = Components.interfaces;
  46. const Cc = Components.classes;
  47. const Cr = Components.results;
  48. Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  49. Components.utils.import("resource://gre/modules/ISO8601DateUtils.jsm");
  50.  
  51. const FP_CONTRACTID = "@mozilla.org/feed-processor;1";
  52. const FP_CLASSID = Components.ID("{26acb1f0-28fc-43bc-867a-a46aabc85dd4}");
  53. const FP_CLASSNAME = "Feed Processor";
  54. const FR_CONTRACTID = "@mozilla.org/feed-result;1";
  55. const FR_CLASSID = Components.ID("{072a5c3d-30c6-4f07-b87f-9f63d51403f2}");
  56. const FR_CLASSNAME = "Feed Result";
  57. const FEED_CONTRACTID = "@mozilla.org/feed;1";
  58. const FEED_CLASSID = Components.ID("{5d0cfa97-69dd-4e5e-ac84-f253162e8f9a}");
  59. const FEED_CLASSNAME = "Feed";
  60. const ENTRY_CONTRACTID = "@mozilla.org/feed-entry;1";
  61. const ENTRY_CLASSID = Components.ID("{8e4444ff-8e99-4bdd-aa7f-fb3c1c77319f}");
  62. const ENTRY_CLASSNAME = "Feed Entry";
  63. const TEXTCONSTRUCT_CONTRACTID = "@mozilla.org/feed-textconstruct;1";
  64. const TEXTCONSTRUCT_CLASSID =
  65.   Components.ID("{b992ddcd-3899-4320-9909-924b3e72c922}");
  66. const TEXTCONSTRUCT_CLASSNAME = "Feed Text Construct";
  67. const GENERATOR_CONTRACTID = "@mozilla.org/feed-generator;1";
  68. const GENERATOR_CLASSID =
  69.   Components.ID("{414af362-9ad8-4296-898e-62247f25a20e}");
  70. const GENERATOR_CLASSNAME = "Feed Generator";
  71. const PERSON_CONTRACTID = "@mozilla.org/feed-person;1";
  72. const PERSON_CLASSID = Components.ID("{95c963b7-20b2-11db-92f6-001422106990}");
  73. const PERSON_CLASSNAME = "Feed Person";
  74.  
  75. const IO_CONTRACTID = "@mozilla.org/network/io-service;1"
  76. const BAG_CONTRACTID = "@mozilla.org/hash-property-bag;1"
  77. const ARRAY_CONTRACTID = "@mozilla.org/array;1";
  78. const SAX_CONTRACTID = "@mozilla.org/saxparser/xmlreader;1";
  79. const UNESCAPE_CONTRACTID = "@mozilla.org/feed-unescapehtml;1";
  80.  
  81.  
  82. var gIoService = null;
  83.  
  84. const XMLNS = "http://www.w3.org/XML/1998/namespace";
  85. const RSS090NS = "http://my.netscape.com/rdf/simple/0.9/";
  86. const WAIROLE_NS = "http://www.w3.org/2005/01/wai-rdf/GUIRoleTaxonomy#";
  87.  
  88. /***** Some general utils *****/
  89. function strToURI(link, base) {
  90.   var base = base || null;
  91.   if (!gIoService)
  92.     gIoService = Cc[IO_CONTRACTID].getService(Ci.nsIIOService);
  93.   try {
  94.     return gIoService.newURI(link, null, base);
  95.   }
  96.   catch(e) {
  97.     return null;
  98.   }
  99. }
  100.  
  101. function isArray(a) {
  102.   return isObject(a) && a.constructor == Array;
  103. }
  104.  
  105. function isObject(a) {
  106.   return (a && typeof a == "object") || isFunction(a);
  107. }
  108.  
  109. function isFunction(a) {
  110.   return typeof a == "function";
  111. }
  112.  
  113. function isIID(a, iid) {
  114.   var rv = false;
  115.   try {
  116.     a.QueryInterface(iid);
  117.     rv = true;
  118.   }
  119.   catch(e) {
  120.   }
  121.   return rv;
  122. }
  123.  
  124. function isIArray(a) {
  125.   return isIID(a, Ci.nsIArray);
  126. }
  127.  
  128. function isIFeedContainer(a) {
  129.   return isIID(a, Ci.nsIFeedContainer);
  130. }
  131.  
  132. function stripTags(someHTML) {
  133.   return someHTML.replace(/<[^>]+>/g,"");
  134. }
  135.  
  136. /**
  137.  * Searches through an array of links and returns a JS array 
  138.  * of matching property bags.
  139.  */
  140. const IANA_URI = "http://www.iana.org/assignments/relation/";
  141. function findAtomLinks(rel, links) {
  142.   var rvLinks = [];
  143.   for (var i = 0; i < links.length; ++i) {
  144.     var linkElement = links.queryElementAt(i, Ci.nsIPropertyBag2);
  145.     // atom:link MUST have @href
  146.     if (bagHasKey(linkElement, "href")) {
  147.       var relAttribute = null;
  148.       if (bagHasKey(linkElement, "rel"))
  149.         relAttribute = linkElement.getPropertyAsAString("rel")
  150.       if ((!relAttribute && rel == "alternate") || relAttribute == rel) {
  151.         rvLinks.push(linkElement);
  152.         continue;
  153.       }
  154.       // catch relations specified by IANA URI 
  155.       if (relAttribute == IANA_URI + rel) {
  156.         rvLinks.push(linkElement);
  157.       }
  158.     }
  159.   }
  160.   return rvLinks;
  161. }
  162.  
  163. function xmlEscape(s) {
  164.   s = s.replace(/&/g, "&");
  165.   s = s.replace(/>/g, ">");
  166.   s = s.replace(/</g, "<");
  167.   s = s.replace(/"/g, """);
  168.   s = s.replace(/'/g, "'");
  169.   return s;
  170. }
  171.  
  172. function arrayContains(array, element) {
  173.   for (var i = 0; i < array.length; ++i) {
  174.     if (array[i] == element) {
  175.       return true;
  176.     }
  177.   }
  178.   return false;
  179. }
  180.  
  181. // XXX add hasKey to nsIPropertyBag
  182. function bagHasKey(bag, key) {
  183.   try {
  184.     bag.getProperty(key);
  185.     return true;
  186.   }
  187.   catch (e) {
  188.     return false;
  189.   }
  190. }
  191.  
  192. function makePropGetter(key) {
  193.   return function FeedPropGetter(bag) {
  194.     try {
  195.       return value = bag.getProperty(key);
  196.     }
  197.     catch(e) {
  198.     }
  199.     return null;
  200.   }
  201. }
  202.  
  203. function W3CToIETFDate(dateString) {
  204.   var date = ISO8601DateUtils.parse(dateString);
  205.   return date.toUTCString();
  206. }
  207.  
  208. const RDF_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
  209. // namespace map
  210. var gNamespaces = {
  211.   "http://webns.net/mvcb/":"admin",
  212.   "http://backend.userland.com/rss":"",
  213.   "http://blogs.law.harvard.edu/tech/rss":"",
  214.   "http://www.w3.org/2005/Atom":"atom",
  215.   "http://purl.org/atom/ns#":"atom03",
  216.   "http://purl.org/rss/1.0/modules/content/":"content",
  217.   "http://purl.org/dc/elements/1.1/":"dc",
  218.   "http://purl.org/dc/terms/":"dcterms",
  219.   "http://www.w3.org/1999/02/22-rdf-syntax-ns#":"rdf",
  220.   "http://purl.org/rss/1.0/":"rss1",
  221.   "http://my.netscape.com/rdf/simple/0.9/":"rss1",
  222.   "http://wellformedweb.org/CommentAPI/":"wfw",                              
  223.   "http://purl.org/rss/1.0/modules/wiki/":"wiki", 
  224.   "http://www.w3.org/XML/1998/namespace":"xml",
  225.   "http://search.yahoo.com/mrss/":"media",
  226.   "http://search.yahoo.com/mrss":"media"
  227. }
  228.  
  229. // We allow a very small set of namespaces in XHTML content,
  230. // for attributes only
  231. var gAllowedXHTMLNamespaces = {
  232.   "http://www.w3.org/XML/1998/namespace":"xml",
  233.   "http://www.w3.org/TR/xhtml2":"xhtml2",
  234.   "http://www.w3.org/2005/07/aaa":"aaa",
  235.   // if someone ns qualifies XHTML, we have to prefix it to avoid an
  236.   // attribute collision.
  237.   "http://www.w3.org/1999/xhtml":"xhtml"
  238. }
  239.  
  240. function FeedResult() {}
  241. FeedResult.prototype = {
  242.   bozo: false,
  243.   doc: null,
  244.   version: null,
  245.   headers: null,
  246.   uri: null,
  247.   stylesheet: null,
  248.  
  249.   registerExtensionPrefix: function FR_registerExtensionPrefix(ns, prefix) {
  250.     throw Cr.NS_ERROR_NOT_IMPLEMENTED;
  251.   },
  252.  
  253.   // XPCOM stuff
  254.   classDescription: FR_CLASSNAME,
  255.   classID: FR_CLASSID,
  256.   contractID: FR_CONTRACTID,
  257.   QueryInterface: XPCOMUtils.generateQI([Ci.nsIFeedResult])
  258. }  
  259.  
  260. function Feed() {
  261.   this.subtitle = null;
  262.   this.title = null;
  263.   this.items = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray);
  264.   this.link = null;
  265.   this.id = null;
  266.   this.generator = null;
  267.   this.authors = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray);
  268.   this.contributors = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray);
  269.   this.baseURI = null;
  270.   this.enclosureCount = 0;
  271.   this.type = Ci.nsIFeed.TYPE_FEED;
  272. }
  273.  
  274. Feed.prototype = {
  275.   searchLists: {
  276.     title: ["title", "rss1:title", "atom03:title", "atom:title"],
  277.     subtitle: ["description","dc:description","rss1:description",
  278.                "atom03:tagline","atom:subtitle"],
  279.     items: ["items","atom03_entries","entries"],
  280.     id: ["atom:id","rdf:about"],
  281.     generator: ["generator"],
  282.     authors : ["authors"],
  283.     contributors: ["contributors"],
  284.     title: ["title","rss1:title", "atom03:title","atom:title"],
  285.     link:  [["link",strToURI],["rss1:link",strToURI]],
  286.     categories: ["categories", "dc:subject"],
  287.     rights: ["atom03:rights","atom:rights"],
  288.     cloud: ["cloud"],
  289.     image: ["image", "rss1:image", "atom:logo"],
  290.     textInput: ["textInput", "rss1:textinput"],
  291.     skipDays: ["skipDays"],
  292.     skipHours: ["skipHours"],
  293.     updated: ["pubDate", "lastBuildDate", "atom03:modified", "dc:date",
  294.               "dcterms:modified", "atom:updated"]
  295.   },
  296.  
  297.   normalize: function Feed_normalize() {
  298.     fieldsToObj(this, this.searchLists);
  299.     if (this.skipDays)
  300.       this.skipDays = this.skipDays.getProperty("days");
  301.     if (this.skipHours)
  302.       this.skipHours = this.skipHours.getProperty("hours");
  303.  
  304.     if (this.updated)
  305.       this.updated = dateParse(this.updated);
  306.  
  307.     // Assign Atom link if needed
  308.     if (bagHasKey(this.fields, "links"))
  309.       this._atomLinksToURI();
  310.  
  311.     this._calcEnclosureCountAndFeedType();
  312.  
  313.     // Resolve relative image links
  314.     if (this.image && bagHasKey(this.image, "url"))
  315.       this._resolveImageLink();
  316.  
  317.     this._resetBagMembersToRawText([this.searchLists.subtitle, 
  318.                                     this.searchLists.title]);
  319.   },
  320.  
  321.   _calcEnclosureCountAndFeedType: function Feed_calcEnclosureCountAndFeedType() {
  322.     var entries_with_enclosures = 0;
  323.     var audio_count = 0;
  324.     var image_count = 0;
  325.     var video_count = 0;
  326.     var other_count = 0;
  327.  
  328.     for (var i = 0; i < this.items.length; ++i) {
  329.       var entry = this.items.queryElementAt(i, Ci.nsIFeedEntry);
  330.       entry.QueryInterface(Ci.nsIFeedContainer);
  331.  
  332.       if (entry.enclosures && entry.enclosures.length > 0) {
  333.         ++entries_with_enclosures;
  334.  
  335.         for (var e = 0; e < entry.enclosures.length; ++e) {
  336.           var enc = entry.enclosures.queryElementAt(e, Ci.nsIWritablePropertyBag2);
  337.           if (enc.hasKey("type")) {
  338.             var enctype = enc.get("type");
  339.  
  340.             if (/^audio/.test(enctype)) {
  341.               ++audio_count;
  342.             } else if (/^image/.test(enctype)) {
  343.               ++image_count;
  344.             } else if (/^video/.test(enctype)) {
  345.               ++video_count;
  346.             } else {
  347.               ++other_count;
  348.             }
  349.           } else {
  350.             ++other_count;
  351.           }
  352.         }
  353.       }
  354.     }
  355.  
  356.     var feedtype = Ci.nsIFeed.TYPE_FEED;
  357.  
  358.     // For a feed to be marked as TYPE_VIDEO, TYPE_AUDIO and TYPE_IMAGE, 
  359.     // we enforce two things:
  360.     //
  361.     //    1. all entries must have at least one enclosure
  362.     //    2. all enclosures must be video for TYPE_VIDEO, audio for TYPE_AUDIO or image
  363.     //       for TYPE_IMAGE
  364.     //
  365.     // Otherwise it's a TYPE_FEED.
  366.     if (entries_with_enclosures == this.items.length && other_count == 0) {
  367.       if (audio_count > 0 && !video_count && !image_count) {
  368.         feedtype = Ci.nsIFeed.TYPE_AUDIO;
  369.  
  370.       } else if (image_count > 0 && !audio_count && !video_count) {
  371.         feedtype = Ci.nsIFeed.TYPE_IMAGE;
  372.  
  373.       } else if (video_count > 0 && !audio_count && !image_count) {
  374.         feedtype = Ci.nsIFeed.TYPE_VIDEO;
  375.       }
  376.     }
  377.  
  378.     this.type = feedtype;
  379.     this.enclosureCount = other_count + video_count + audio_count + image_count;
  380.   },
  381.  
  382.   _atomLinksToURI: function Feed_linkToURI() {
  383.     var links = this.fields.getPropertyAsInterface("links", Ci.nsIArray);
  384.     var alternates = findAtomLinks("alternate", links);
  385.     if (alternates.length > 0) {
  386.       var href = alternates[0].getPropertyAsAString("href");
  387.       var base;
  388.       if (bagHasKey(alternates[0], "xml:base"))
  389.         base = alternates[0].getPropertyAsAString("xml:base");
  390.       this.link = this._resolveURI(href, base);
  391.     }
  392.   },
  393.  
  394.   _resolveImageLink: function Feed_resolveImageLink() {
  395.     var base;
  396.     if (bagHasKey(this.image, "xml:base"))
  397.       base = this.image.getPropertyAsAString("xml:base");
  398.     var url = this._resolveURI(this.image.getPropertyAsAString("url"), base);
  399.     if (url)
  400.       this.image.setPropertyAsAString("url", url.spec);
  401.   },
  402.  
  403.   _resolveURI: function Feed_resolveURI(linkSpec, baseSpec) {
  404.     var uri = null;
  405.     try {
  406.       var base = baseSpec ? strToURI(baseSpec, this.baseURI) : this.baseURI;
  407.       uri = strToURI(linkSpec, base);
  408.     }
  409.     catch(e) {
  410.       LOG(e);
  411.     }
  412.  
  413.     return uri;
  414.   },
  415.  
  416.   // reset the bag to raw contents, not text constructs
  417.   _resetBagMembersToRawText: function Feed_resetBagMembers(fieldLists) {
  418.     for (var i=0; i<fieldLists.length; i++) {      
  419.       for (var j=0; j<fieldLists[i].length; j++) {
  420.         if (bagHasKey(this.fields, fieldLists[i][j])) {
  421.           var textConstruct = this.fields.getProperty(fieldLists[i][j]);
  422.           this.fields.setPropertyAsAString(fieldLists[i][j],
  423.                                            textConstruct.text);
  424.         }
  425.       }
  426.     }
  427.   },
  428.   
  429.   // XPCOM stuff
  430.   classDescription: FEED_CLASSNAME,
  431.   classID: FEED_CLASSID,
  432.   contractID: FEED_CONTRACTID,
  433.   QueryInterface: XPCOMUtils.generateQI([Ci.nsIFeed, Ci.nsIFeedContainer])
  434. }
  435.  
  436. function Entry() {
  437.   this.summary = null;
  438.   this.content = null;
  439.   this.title = null;
  440.   this.fields = Cc["@mozilla.org/hash-property-bag;1"].
  441.     createInstance(Ci.nsIWritablePropertyBag2);
  442.   this.link = null;
  443.   this.id = null;
  444.   this.baseURI = null;
  445.   this.updated = null;
  446.   this.published = null;
  447.   this.authors = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray);
  448.   this.contributors = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray);
  449. }
  450.   
  451. Entry.prototype = {
  452.   fields: null,
  453.   enclosures: null,
  454.   mediaContent: null,
  455.   
  456.   searchLists: {
  457.     title: ["title", "rss1:title", "atom03:title", "atom:title"],
  458.     link: [["link",strToURI],["rss1:link",strToURI]],
  459.     id: [["guid", makePropGetter("guid")], "rdf:about",
  460.          "atom03:id", "atom:id"],
  461.     authors : ["authors"],
  462.     contributors: ["contributors"],
  463.     summary: ["description", "rss1:description", "dc:description",
  464.               "atom03:summary", "atom:summary"],
  465.     content: ["content:encoded","atom03:content","atom:content"],
  466.     rights: ["atom03:rights","atom:rights"],
  467.     published: ["pubDate", "atom03:issued", "dcterms:issued", "atom:published"],
  468.     updated: ["pubDate", "atom03:modified", "dc:date", "dcterms:modified",
  469.               "atom:updated"]
  470.   },
  471.   
  472.   normalize: function Entry_normalize() {
  473.     fieldsToObj(this, this.searchLists);
  474.  
  475.     // Assign Atom link if needed
  476.     if (bagHasKey(this.fields, "links"))
  477.       this._atomLinksToURI();
  478.  
  479.     // Populate enclosures array
  480.     this._populateEnclosures();
  481.  
  482.     // The link might be a guid w/ permalink=true
  483.     if (!this.link && bagHasKey(this.fields, "guid")) {
  484.       var guid = this.fields.getProperty("guid");
  485.       var isPermaLink = true;
  486.       
  487.       if (bagHasKey(guid, "isPermaLink"))
  488.         isPermaLink = guid.getProperty("isPermaLink").toLowerCase() != "false";
  489.       
  490.       if (guid && isPermaLink)
  491.         this.link = strToURI(guid.getProperty("guid"));
  492.     }
  493.  
  494.     if (this.updated)
  495.       this.updated = dateParse(this.updated);
  496.     if (this.published)
  497.       this.published = dateParse(this.published);
  498.  
  499.     this._resetBagMembersToRawText([this.searchLists.content, 
  500.                                     this.searchLists.summary, 
  501.                                     this.searchLists.title]);
  502.   },
  503.  
  504.   _populateEnclosures: function Entry_populateEnclosures() {
  505.     if (bagHasKey(this.fields, "links"))
  506.       this._atomLinksToEnclosures();
  507.  
  508.     // Add RSS2 enclosure to enclosures
  509.     if (bagHasKey(this.fields, "enclosure"))
  510.       this._enclosureToEnclosures();
  511.  
  512.     // Add media:content to enclosures
  513.     if (bagHasKey(this.fields, "mediacontent"))
  514.       this._mediacontentToEnclosures();
  515.  
  516.     // Add media:content in media:group to enclosures
  517.     if (bagHasKey(this.fields, "mediagroup"))
  518.       this._mediagroupToEnclosures();
  519.   },
  520.  
  521.   __enclosure_map: null,
  522.  
  523.   _addToEnclosures: function Entry_addToEnclosures(new_enc) {
  524.     // items we add to the enclosures array get displayed in the FeedWriter and
  525.     // they must have non-empty urls.
  526.     if (!bagHasKey(new_enc, "url") || new_enc.getPropertyAsAString("url") == "")
  527.       return;
  528.  
  529.     if (this.__enclosure_map == null)
  530.       this.__enclosure_map = {};
  531.  
  532.     var previous_enc = this.__enclosure_map[new_enc.getPropertyAsAString("url")];
  533.  
  534.     if (previous_enc != undefined) {
  535.       previous_enc.QueryInterface(Ci.nsIWritablePropertyBag2);
  536.  
  537.       if (!bagHasKey(previous_enc, "type") && bagHasKey(new_enc, "type"))
  538.         previous_enc.setPropertyAsAString("type", new_enc.getPropertyAsAString("type"));
  539.  
  540.       if (!bagHasKey(previous_enc, "length") && bagHasKey(new_enc, "length"))
  541.         previous_enc.setPropertyAsAString("length", new_enc.getPropertyAsAString("length"));
  542.       
  543.       return;
  544.     }
  545.  
  546.     if (this.enclosures == null) {
  547.       this.enclosures = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray);
  548.       this.enclosures.QueryInterface(Ci.nsIMutableArray);
  549.     }
  550.  
  551.     this.enclosures.appendElement(new_enc, false);
  552.     this.__enclosure_map[new_enc.getPropertyAsAString("url")] = new_enc;
  553.   },
  554.  
  555.   _atomLinksToEnclosures: function Entry_linkToEnclosure() {
  556.     var links = this.fields.getPropertyAsInterface("links", Ci.nsIArray);
  557.     var enc_links = findAtomLinks("enclosure", links);
  558.     if (enc_links.length == 0)
  559.       return;
  560.  
  561.     for (var i = 0; i < enc_links.length; ++i) {
  562.       var link = enc_links[i];
  563.  
  564.       // an enclosure must have an href
  565.       if (!(link.getProperty("href")))
  566.         return;
  567.  
  568.       var enc = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2);
  569.  
  570.       // copy Atom bits over to equivalent enclosure bits
  571.       enc.setPropertyAsAString("url", link.getPropertyAsAString("href"));
  572.       if (bagHasKey(link, "type"))
  573.         enc.setPropertyAsAString("type", link.getPropertyAsAString("type"));
  574.       if (bagHasKey(link, "length"))
  575.         enc.setPropertyAsAString("length", link.getPropertyAsAString("length"));
  576.  
  577.       this._addToEnclosures(enc);
  578.     }
  579.   },
  580.  
  581.   _enclosureToEnclosures: function Entry_enclosureToEnclosures() {
  582.     var enc = this.fields.getPropertyAsInterface("enclosure", Ci.nsIPropertyBag2);
  583.  
  584.     if (!(enc.getProperty("url")))
  585.       return;
  586.  
  587.     this._addToEnclosures(enc);
  588.   },
  589.  
  590.   _mediacontentToEnclosures: function Entry_mediacontentToEnclosures() {
  591.     var mediacontent = this.fields.getPropertyAsInterface("mediacontent", Ci.nsIArray);
  592.  
  593.     for (var i = 0; i < mediacontent.length; ++i) {
  594.       var contentElement = mediacontent.queryElementAt(i, Ci.nsIWritablePropertyBag2);
  595.  
  596.       // media:content don't require url, but if it's not there, we should
  597.       // skip it.
  598.       if (!bagHasKey(contentElement, "url"))
  599.         continue;
  600.  
  601.       var enc = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2);
  602.  
  603.       // copy media:content bits over to equivalent enclosure bits
  604.       enc.setPropertyAsAString("url", contentElement.getPropertyAsAString("url"));
  605.       if (bagHasKey(contentElement, "type")) {
  606.         enc.setPropertyAsAString("type", contentElement.getPropertyAsAString("type"));
  607.       }
  608.       if (bagHasKey(contentElement, "fileSize")) {
  609.         enc.setPropertyAsAString("length", contentElement.getPropertyAsAString("fileSize"));
  610.       }
  611.  
  612.       this._addToEnclosures(enc);
  613.     }
  614.   },
  615.  
  616.   _mediagroupToEnclosures: function Entry_mediagroupToEnclosures() {
  617.     var group = this.fields.getPropertyAsInterface("mediagroup", Ci.nsIPropertyBag2);
  618.  
  619.     var content = group.getPropertyAsInterface("mediacontent", Ci.nsIArray);
  620.     for (var i = 0; i < content.length; ++i) {
  621.       var contentElement = content.queryElementAt(i, Ci.nsIWritablePropertyBag2);
  622.       // media:content don't require url, but if it's not there, we should
  623.       // skip it.
  624.       if (!bagHasKey(contentElement, "url"))
  625.         continue;
  626.  
  627.       var enc = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2);
  628.  
  629.       // copy media:content bits over to equivalent enclosure bits
  630.       enc.setPropertyAsAString("url", contentElement.getPropertyAsAString("url"));
  631.       if (bagHasKey(contentElement, "type")) {
  632.         enc.setPropertyAsAString("type", contentElement.getPropertyAsAString("type"));
  633.       }
  634.       if (bagHasKey(contentElement, "fileSize")) {
  635.         enc.setPropertyAsAString("length", contentElement.getPropertyAsAString("fileSize"));
  636.       }
  637.  
  638.       this._addToEnclosures(enc);
  639.     }
  640.   },
  641.  
  642.   // XPCOM stuff
  643.   classDescription: ENTRY_CLASSNAME,
  644.   classID: ENTRY_CLASSID,
  645.   contractID: ENTRY_CONTRACTID,
  646.   QueryInterface: XPCOMUtils.generateQI(
  647.     [Ci.nsIFeedEntry, Ci.nsIFeedContainer]
  648.   )
  649. }
  650.  
  651. Entry.prototype._atomLinksToURI = Feed.prototype._atomLinksToURI;
  652. Entry.prototype._resolveURI = Feed.prototype._resolveURI;
  653. Entry.prototype._resetBagMembersToRawText = 
  654.    Feed.prototype._resetBagMembersToRawText;
  655.  
  656. // TextConstruct represents and element that could contain (X)HTML
  657. function TextConstruct() {
  658.   this.lang = null;
  659.   this.base = null;
  660.   this.type = "text";
  661.   this.text = null;
  662.   this.unescapeHTML = Cc[UNESCAPE_CONTRACTID].
  663.                       getService(Ci.nsIScriptableUnescapeHTML);
  664. }
  665.  
  666. TextConstruct.prototype = {
  667.   plainText: function TC_plainText() {
  668.     if (this.type != "text") {
  669.       return this.unescapeHTML.unescape(stripTags(this.text));
  670.     }
  671.     return this.text;
  672.   },
  673.  
  674.   createDocumentFragment: function TC_createDocumentFragment(element) {
  675.     if (this.type == "text") {
  676.       var doc = element.ownerDocument;
  677.       var docFragment = doc.createDocumentFragment();
  678.       var node = doc.createTextNode(this.text);
  679.       docFragment.appendChild(node);
  680.       return docFragment;
  681.     }
  682.     var isXML;
  683.     if (this.type == "xhtml")
  684.       isXML = true
  685.     else if (this.type == "html")
  686.       isXML = false;
  687.     else
  688.       return null;
  689.  
  690.     return this.unescapeHTML.parseFragment(this.text, isXML,
  691.                                            this.base, element);
  692.   },
  693.  
  694.   // XPCOM stuff
  695.   classDescription: TEXTCONSTRUCT_CLASSNAME,
  696.   classID: TEXTCONSTRUCT_CLASSID,
  697.   contractID: TEXTCONSTRUCT_CONTRACTID,
  698.   QueryInterface: XPCOMUtils.generateQI([Ci.nsIFeedTextConstruct])
  699. }
  700.  
  701. // Generator represents the software that produced the feed
  702. function Generator() {
  703.   this.lang = null;
  704.   this.agent = null;
  705.   this.version = null;
  706.   this.uri = null;
  707.  
  708.   // nsIFeedElementBase
  709.   this._attributes = null;
  710.   this.baseURI = null;
  711. }
  712.  
  713. Generator.prototype = {
  714.  
  715.   get attributes() {
  716.     return this._attributes;
  717.   },
  718.  
  719.   set attributes(value) {
  720.     this._attributes = value;
  721.     this.version = this._attributes.getValueFromName("","version");
  722.     var uriAttribute = this._attributes.getValueFromName("","uri") ||
  723.                        this._attributes.getValueFromName("","url");
  724.     this.uri = strToURI(uriAttribute, this.baseURI);
  725.  
  726.     // RSS1
  727.     uriAttribute = this._attributes.getValueFromName(RDF_NS,"resource");
  728.     if (uriAttribute) {
  729.       this.agent = uriAttribute;
  730.       this.uri = strToURI(uriAttribute, this.baseURI);
  731.     }
  732.   },
  733.  
  734.   // XPCOM stuff
  735.   classDescription: GENERATOR_CLASSNAME,
  736.   classID: GENERATOR_CLASSID,
  737.   contractID: GENERATOR_CONTRACTID,
  738.   QueryInterface: XPCOMUtils.generateQI(
  739.     [Ci.nsIFeedGenerator, Ci.nsIFeedElementBase]
  740.   )
  741. }
  742.  
  743. function Person() {
  744.   this.name = null;
  745.   this.uri = null;
  746.   this.email = null;
  747.  
  748.   // nsIFeedElementBase
  749.   this.attributes = null;
  750.   this.baseURI = null;
  751. }
  752.  
  753. Person.prototype = {
  754.   // XPCOM stuff
  755.   classDescription: PERSON_CLASSNAME,
  756.   classID: PERSON_CLASSID,
  757.   contractID: PERSON_CONTRACTID,
  758.   QueryInterface: XPCOMUtils.generateQI(
  759.     [Ci.nsIFeedPerson, Ci.nsIFeedElementBase]
  760.   )
  761. }
  762.  
  763. /** 
  764.  * Map a list of fields into properties on a container.
  765.  *
  766.  * @param container An nsIFeedContainer
  767.  * @param fields A list of fields to search for. List members can
  768.  *               be a list, in which case the second member is 
  769.  *               transformation function (like parseInt).
  770.  */
  771. function fieldsToObj(container, fields) {
  772.   var props,prop,field,searchList;
  773.   for (var key in fields) {
  774.     searchList = fields[key];
  775.     for (var i=0; i < searchList.length; ++i) {
  776.       props = searchList[i];
  777.       prop = null;
  778.       field = isArray(props) ? props[0] : props;
  779.       try {
  780.         prop = container.fields.getProperty(field);
  781.       } 
  782.       catch(e) { 
  783.       }
  784.       if (prop) {
  785.         prop = isArray(props) ? props[1](prop) : prop;
  786.         container[key] = prop;
  787.       }
  788.     }
  789.   }
  790. }
  791.  
  792. /**
  793.  * Lower cases an element's localName property
  794.  * @param   element A DOM element.
  795.  *
  796.  * @returns The lower case localName property of the specified element
  797.  */
  798. function LC(element) {
  799.   return element.localName.toLowerCase();
  800. }
  801.  
  802. // TODO move these post-processor functions
  803. // create a generator element
  804. function atomGenerator(s, generator) {
  805.   generator.QueryInterface(Ci.nsIFeedGenerator);
  806.   generator.agent = trimString(s);
  807.   return generator;
  808. }
  809.  
  810. // post-process atom:logo to create an RSS2-like structure
  811. function atomLogo(s, logo) {
  812.   logo.setPropertyAsAString("url", trimString(s));
  813. }
  814.  
  815. // post-process an RSS category, map it to the Atom fields.
  816. function rssCatTerm(s, cat) {
  817.   // add slash handling?
  818.   cat.setPropertyAsAString("term", trimString(s));
  819.   return cat;
  820.  
  821. // post-process a GUID 
  822. function rssGuid(s, guid) {
  823.   guid.setPropertyAsAString("guid", trimString(s));
  824.   return guid;
  825. }
  826.  
  827. // post-process an RSS author element
  828. //
  829. // It can contain a field like this:
  830. // 
  831. //  <author>lawyer@boyer.net (Lawyer Boyer)</author>
  832. //
  833. // or, delightfully, a field like this:
  834. //
  835. //  <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
  836. //
  837. // We want to split this up and assign it to corresponding Atom
  838. // fields.
  839. //
  840. function rssAuthor(s,author) {
  841.   author.QueryInterface(Ci.nsIFeedPerson);
  842.   // check for RSS2 string format
  843.   var chars = trimString(s);
  844.   var matches = chars.match(/(.*)\((.*)\)/);
  845.   var emailCheck = 
  846.     /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
  847.   if (matches) {
  848.     var match1 = trimString(matches[1]);
  849.     var match2 = trimString(matches[2]);
  850.     if (match2.indexOf("mailto:") == 0)
  851.       match2 = match2.substring(7);
  852.     if (emailCheck.test(match1)) {
  853.       author.email = match1;
  854.       author.name = match2;
  855.     }
  856.     else if (emailCheck.test(match2)) {
  857.       author.email = match2;
  858.       author.name = match1;
  859.     }
  860.     else {
  861.       // put it back together
  862.       author.name = match1 + " (" + match2 + ")";
  863.     }
  864.   }
  865.   else {
  866.     author.name = chars;
  867.     if (chars.indexOf('@'))
  868.       author.email = chars;
  869.   }
  870.   return author;
  871. }
  872.  
  873. //
  874. // skipHours and skipDays map to arrays, so we need to change the
  875. // string to an nsISupports in order to stick it in there.
  876. //
  877. function rssArrayElement(s) {
  878.   var str = Cc["@mozilla.org/supports-string;1"].
  879.               createInstance(Ci.nsISupportsString);
  880.   str.data = s;
  881.   str.QueryInterface(Ci.nsISupportsString);
  882.   return str;
  883. }
  884.  
  885. /***** Some feed utils from TBird *****/
  886.  
  887. /**
  888.  * Tests a RFC822 date against a regex.
  889.  * @param aDateStr A string to test as an RFC822 date.
  890.  *
  891.  * @returns A boolean indicating whether the string is a valid RFC822 date.
  892.  */
  893. function isValidRFC822Date(aDateStr) {
  894.   var regex = new RegExp(RFC822_RE);
  895.   return regex.test(aDateStr);
  896. }
  897.  
  898. /**
  899.  * Removes leading and trailing whitespace from a string.
  900.  * @param s The string to trim.
  901.  *
  902.  * @returns A new string with whitespace stripped.
  903.  */
  904. function trimString(s) {
  905.   return(s.replace(/^\s+/, "").replace(/\s+$/, ""));
  906. }
  907.  
  908. // Regular expression matching RFC822 dates 
  909. const RFC822_RE = "^((Mon|Tue|Wed|Thu|Fri|Sat|Sun)([a-z]+)?,? *)?\\d\\d?"
  910. + " +(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)([a-z]+)?"
  911. + " +\\d\\d(\\d\\d)? +\\d?\\d:\\d\\d(:\\d\\d)?"
  912. + " +([+-]?\\d\\d\\d\\d|GMT|UT[C]?|(E|C|M|P)(ST|DT)|[A-IK-Z])$";
  913.  
  914. /**
  915.  * XXX -- need to decide what this should return. 
  916.  * XXX -- Is there a Date class usable from C++?
  917.  *
  918.  * Tries tries parsing various date formats.
  919.  * @param dateString
  920.  *        A string that is supposedly an RFC822 or RFC3339 date.
  921.  * @returns A Date.toString XXX--fixme
  922.  */
  923. function dateParse(dateString) {
  924.   var date = trimString(dateString);
  925.  
  926.   if (date.search(/^\d\d\d\d/) != -1) //Could be a ISO8601/W3C date
  927.     return W3CToIETFDate(dateString);
  928.  
  929.   if (isValidRFC822Date(date))
  930.     return date; 
  931.   
  932.   if (!isNaN(parseInt(date, 10))) { 
  933.     //It's an integer, so maybe it's a timestamp
  934.     var d = new Date(parseInt(date, 10) * 1000);
  935.     var now = new Date();
  936.     var yeardiff = now.getFullYear() - d.getFullYear();
  937.     if ((yeardiff >= 0) && (yeardiff < 3)) {
  938.       // it's quite likely the correct date. 3 years is an arbitrary cutoff,
  939.       // but this is an invalid date format, and there's no way to verify
  940.       // its correctness.
  941.       return d.toString();
  942.     }
  943.   }
  944.   // Can't help.
  945.   return null;
  946.  
  947.  
  948. const XHTML_NS = "http://www.w3.org/1999/xhtml";
  949.  
  950. // The XHTMLHandler handles inline XHTML found in things like atom:summary
  951. function XHTMLHandler(processor, isAtom, waiPrefixes) {
  952.   this._buf = "";
  953.   this._processor = processor;
  954.   this._depth = 0;
  955.   this._isAtom = isAtom;
  956.   // a stack of lists tracking in-scope namespaces
  957.   this._inScopeNS = [];
  958.   this._waiPrefixes = waiPrefixes;
  959. }
  960.  
  961. // The fidelity can be improved here, to allow handling of stuff like
  962. // SVG and MathML. XXX
  963. XHTMLHandler.prototype = {
  964.  
  965.    // look back up at the declared namespaces 
  966.    // we always use the same prefixes for our safe stuff
  967.   _isInScope: function XH__isInScope(ns) {
  968.     for (var i in this._inScopeNS) {
  969.       for (var uri in this._inScopeNS[i]) {
  970.         if (this._inScopeNS[i][uri] == ns)
  971.           return true;
  972.       }
  973.     }
  974.     return false;
  975.   },
  976.  
  977.   startDocument: function XH_startDocument() {
  978.   },
  979.   endDocument: function XH_endDocument() {
  980.   },
  981.   startElement: function XH_startElement(uri, localName, qName, attributes) {
  982.     ++this._depth;
  983.     this._inScopeNS.push([]);
  984.  
  985.     // RFC4287 requires XHTML to be wrapped in a div that is *not* part of 
  986.     // the content. This prevents people from screwing up namespaces, but
  987.     // we need to skip it here.
  988.     if (this._isAtom && this._depth == 1 && localName == "div")
  989.       return;
  990.  
  991.     // If it's an XHTML element, record it. Otherwise, it's ignored.
  992.     if (uri == XHTML_NS) {
  993.       this._buf += "<" + localName;
  994.       var uri;
  995.       for (var i=0; i < attributes.length; ++i) {
  996.         uri = attributes.getURI(i);
  997.         // XHTML attributes aren't in a namespace
  998.         if (uri == "") { 
  999.           this._buf += (" " + attributes.getLocalName(i) + "='" +
  1000.                         xmlEscape(attributes.getValue(i)) + "'");
  1001.         } else {
  1002.           // write a small set of allowed attribute namespaces
  1003.           var prefix = gAllowedXHTMLNamespaces[uri];
  1004.           if (prefix != null) {
  1005.             // The attribute value we'll attempt to write
  1006.             var attributeValue = xmlEscape(attributes.getValue(i));
  1007.  
  1008.             // More QName abuse from W3C
  1009.             var rolePrefix = "";
  1010.             if (attributes.getLocalName(i) == "role") {
  1011.               for (var aPrefix in this._waiPrefixes) {
  1012.                 if (attributeValue.indexOf(aPrefix + ":") == 0) {     
  1013.                   // Now, due to the terrible layer mismatch 
  1014.                   // that is QNames in content, we have to see
  1015.                   // if the attribute value clashes with our 
  1016.                   // namespace declarations.
  1017.                   var isCollision = false;
  1018.                   for (var uriKey in gAllowedXHTMLNamespaces) {
  1019.                     if (gAllowedXHTMLNamespaces[uriKey] == aPrefix)
  1020.                       isCollision = true;
  1021.                   }
  1022.                   
  1023.                   if (isCollision) {
  1024.                     rolePrefix = aPrefix + i;
  1025.                     attributeValue = 
  1026.                       rolePrefix + ":" + 
  1027.                       attributeValue.substring(aPrefix.length + 1);
  1028.                   } else {
  1029.                     rolePrefix = aPrefix;
  1030.                   }
  1031.  
  1032.                   break;
  1033.                 }
  1034.               }
  1035.  
  1036.               if (rolePrefix)
  1037.                 this._buf += (" xmlns:" + rolePrefix + 
  1038.                               "='" + WAIROLE_NS + "'");
  1039.             }
  1040.  
  1041.             // it's an allowed attribute NS.            
  1042.             // write the attribute
  1043.             this._buf += (" " + prefix + ":" + 
  1044.                           attributes.getLocalName(i) + 
  1045.                           "='" + attributeValue + "'");
  1046.  
  1047.             // write an xmlns declaration if necessary
  1048.             if (prefix != "xml" && !this._isInScope(uri)) {
  1049.               this._inScopeNS[this._inScopeNS.length - 1].push(uri);
  1050.               this._buf += " xmlns:" + prefix + "='" + uri + "'";
  1051.             }
  1052.           }
  1053.         }
  1054.       }
  1055.       this._buf += ">";
  1056.     }
  1057.   },
  1058.   endElement: function XH_endElement(uri, localName, qName) {
  1059.     --this._depth;
  1060.     this._inScopeNS.pop();
  1061.  
  1062.     // We need to skip outer divs in Atom. See comment in startElement.
  1063.     if (this._isAtom && this._depth == 0 && localName == "div")
  1064.       return;
  1065.  
  1066.     // When we peek too far, go back to the main processor
  1067.     if (this._depth < 0) {
  1068.       this._processor.returnFromXHTMLHandler(trimString(this._buf),
  1069.                                              uri, localName, qName);
  1070.       return;
  1071.     }
  1072.     // If it's an XHTML element, record it. Otherwise, it's ignored.
  1073.     if (uri == XHTML_NS) {
  1074.       this._buf += "</" + localName + ">";
  1075.     }
  1076.   },
  1077.   characters: function XH_characters(data) {
  1078.     this._buf += xmlEscape(data);
  1079.   },
  1080.   startPrefixMapping: function XH_startPrefixMapping(prefix, uri) {
  1081.     if (prefix && uri == WAIROLE_NS) 
  1082.       this._waiPrefixes[prefix] = WAIROLE_NS;
  1083.   },
  1084.   endPrefixMapping: function FP_endPrefixMapping(prefix) {
  1085.     if (prefix)
  1086.       delete this._waiPrefixes[prefix];
  1087.   },
  1088.   processingInstruction: function XH_processingInstruction() {
  1089.   }, 
  1090. }
  1091.  
  1092. /**
  1093.  * The ExtensionHandler deals with elements we haven't explicitly
  1094.  * added to our transition table in the FeedProcessor.
  1095.  */
  1096. function ExtensionHandler(processor) {
  1097.   this._buf = "";
  1098.   this._depth = 0;
  1099.   this._hasChildElements = false;
  1100.  
  1101.   // The FeedProcessor
  1102.   this._processor = processor;
  1103.  
  1104.   // Fields of the outermost extension element.
  1105.   this._localName = null;
  1106.   this._uri = null;
  1107.   this._qName = null;
  1108.   this._attrs = null;
  1109. }
  1110.  
  1111. ExtensionHandler.prototype = {
  1112.   startDocument: function EH_startDocument() {
  1113.   },
  1114.   endDocument: function EH_endDocument() {
  1115.   },
  1116.   startElement: function EH_startElement(uri, localName, qName, attrs) {
  1117.     ++this._depth;
  1118.     var prefix = gNamespaces[uri] ? gNamespaces[uri] + ":" : "";
  1119.     var key =  prefix + localName;
  1120.     
  1121.     if (this._depth == 1) {
  1122.       this._uri = uri;
  1123.       this._localName = localName;
  1124.       this._qName = qName;
  1125.       this._attrs = attrs;
  1126.     }
  1127.     
  1128.     // if we descend into another element, we won't send text
  1129.     this._hasChildElements = (this._depth > 1);
  1130.     
  1131.   },
  1132.   endElement: function EH_endElement(uri, localName, qName) {
  1133.     --this._depth;
  1134.     if (this._depth == 0) {
  1135.       var text = this._hasChildElements ? null : trimString(this._buf);
  1136.       this._processor.returnFromExtHandler(this._uri, this._localName, 
  1137.                                            text, this._attrs);
  1138.     }
  1139.   },
  1140.   characters: function EH_characters(data) {
  1141.     if (!this._hasChildElements)
  1142.       this._buf += data;
  1143.   },
  1144.   startPrefixMapping: function EH_startPrefixMapping() {
  1145.   },
  1146.   endPrefixMapping: function EH_endPrefixMapping() {
  1147.   },
  1148.   processingInstruction: function EH_processingInstruction() {
  1149.   }, 
  1150. };
  1151.  
  1152.  
  1153. /**
  1154.  * ElementInfo is a simple container object that describes
  1155.  * some characteristics of a feed element. For example, it
  1156.  * says whether an element can be expected to appear more
  1157.  * than once inside a given entry or feed.
  1158.  */ 
  1159. function ElementInfo(fieldName, containerClass, closeFunc, isArray) {
  1160.   this.fieldName = fieldName;
  1161.   this.containerClass = containerClass;
  1162.   this.closeFunc = closeFunc;
  1163.   this.isArray = isArray;
  1164.   this.isWrapper = false;
  1165. }
  1166.  
  1167. /**
  1168.  * FeedElementInfo represents a feed element, usually the root.
  1169.  */
  1170. function FeedElementInfo(fieldName, feedVersion) {
  1171.   this.isWrapper = false;
  1172.   this.fieldName = fieldName;
  1173.   this.feedVersion = feedVersion;
  1174. }
  1175.  
  1176. /**
  1177.  * Some feed formats include vestigial wrapper elements that we don't
  1178.  * want to include in our object model, but we do need to keep track
  1179.  * of during parsing.
  1180.  */
  1181. function WrapperElementInfo(fieldName) {
  1182.   this.isWrapper = true;
  1183.   this.fieldName = fieldName;
  1184. }
  1185.  
  1186. /***** The Processor *****/
  1187. function FeedProcessor() {
  1188.   this._reader = Cc[SAX_CONTRACTID].createInstance(Ci.nsISAXXMLReader);
  1189.   this._buf =  "";
  1190.   this._feed = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2);
  1191.   this._handlerStack = [];
  1192.   this._xmlBaseStack = []; // sparse array keyed to nesting depth
  1193.   this._depth = 0;
  1194.   this._state = "START";
  1195.   this._result = null;
  1196.   this._extensionHandler = null;
  1197.   this._xhtmlHandler = null;
  1198.   this._haveSentResult = false;
  1199.   
  1200.   // http://www.w3.org/WAI/PF/GUI/ uses QNames in content :(
  1201.   this._waiPrefixes = {};
  1202.  
  1203.   // The nsIFeedResultListener waiting for the parse results
  1204.   this.listener = null;
  1205.  
  1206.   // These elements can contain (X)HTML or plain text.
  1207.   // We keep a table here that contains their default treatment
  1208.   this._textConstructs = {"atom:title":"text",
  1209.                           "atom:summary":"text",
  1210.                           "atom:rights":"text",
  1211.                           "atom:content":"text",
  1212.                           "atom:subtitle":"text",
  1213.                           "description":"html",
  1214.                           "rss1:description":"html",
  1215.                           "dc:description":"html",
  1216.                           "content:encoded":"html",
  1217.                           "title":"text",
  1218.                           "rss1:title":"text",
  1219.                           "atom03:title":"text",
  1220.                           "atom03:tagline":"text",
  1221.                           "atom03:summary":"text",
  1222.                           "atom03:content":"text"};
  1223.   this._stack = [];
  1224.  
  1225.   this._trans = {   
  1226.     "START": {
  1227.       //If we hit a root RSS element, treat as RSS2.
  1228.       "rss": new FeedElementInfo("RSS2", "rss2"),
  1229.  
  1230.       // If we hit an RDF element, if could be RSS1, but we can't
  1231.       // verify that until we hit a rss1:channel element.
  1232.       "rdf:RDF": new WrapperElementInfo("RDF"),
  1233.  
  1234.       // If we hit a Atom 1.0 element, treat as Atom 1.0.
  1235.       "atom:feed": new FeedElementInfo("Atom", "atom"),
  1236.  
  1237.       // Treat as Atom 0.3
  1238.       "atom03:feed": new FeedElementInfo("Atom03", "atom03"),
  1239.     },
  1240.     
  1241.     /********* RSS2 **********/
  1242.     "IN_RSS2": {
  1243.       "channel": new WrapperElementInfo("channel")
  1244.     },
  1245.  
  1246.     "IN_CHANNEL": {
  1247.       "item": new ElementInfo("items", Cc[ENTRY_CONTRACTID], null, true),
  1248.       "managingEditor": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1249.                                         rssAuthor, true),
  1250.       "dc:creator": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1251.                                     rssAuthor, true),
  1252.       "dc:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1253.                                    rssAuthor, true),
  1254.       "dc:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID],
  1255.                                          rssAuthor, true),
  1256.       "category": new ElementInfo("categories", null, rssCatTerm, true),
  1257.       "cloud": new ElementInfo("cloud", null, null, false),
  1258.       "image": new ElementInfo("image", null, null, false),
  1259.       "textInput": new ElementInfo("textInput", null, null, false),
  1260.       "skipDays": new ElementInfo("skipDays", null, null, false),
  1261.       "skipHours": new ElementInfo("skipHours", null, null, false),
  1262.       "generator": new ElementInfo("generator", Cc[GENERATOR_CONTRACTID],
  1263.                                    atomGenerator, false),
  1264.     },
  1265.  
  1266.     "IN_ITEMS": {
  1267.       "author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1268.                                 rssAuthor, true),
  1269.       "dc:creator": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1270.                                     rssAuthor, true),
  1271.       "dc:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1272.                                    rssAuthor, true),
  1273.       "dc:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID],
  1274.                                          rssAuthor, true),
  1275.       "category": new ElementInfo("categories", null, rssCatTerm, true),
  1276.       "enclosure": new ElementInfo("enclosure", null, null, false),
  1277.       "media:content": new ElementInfo("mediacontent", null, null, true),
  1278.       "media:group": new ElementInfo("mediagroup", null, null, false),
  1279.       "guid": new ElementInfo("guid", null, rssGuid, false)
  1280.     },
  1281.  
  1282.     "IN_SKIPDAYS": {
  1283.       "day": new ElementInfo("days", null, rssArrayElement, true)
  1284.     },
  1285.  
  1286.     "IN_SKIPHOURS":{
  1287.       "hour": new ElementInfo("hours", null, rssArrayElement, true)
  1288.     },
  1289.  
  1290.     "IN_MEDIAGROUP": {
  1291.       "media:content": new ElementInfo("mediacontent", null, null, true)
  1292.     },
  1293.  
  1294.     /********* RSS1 **********/
  1295.     "IN_RDF": {
  1296.       // If we hit a rss1:channel, we can verify that we have RSS1
  1297.       "rss1:channel": new FeedElementInfo("rdf_channel", "rss1"),
  1298.       "rss1:image": new ElementInfo("image", null, null, false),
  1299.       "rss1:textinput": new ElementInfo("textInput", null, null, false),
  1300.       "rss1:item": new ElementInfo("items", Cc[ENTRY_CONTRACTID], null, true),
  1301.     },
  1302.  
  1303.     "IN_RDF_CHANNEL": {
  1304.       "admin:generatorAgent": new ElementInfo("generator",
  1305.                                               Cc[GENERATOR_CONTRACTID],
  1306.                                               null, false),
  1307.       "dc:creator": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1308.                                     rssAuthor, true),
  1309.       "dc:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1310.                                    rssAuthor, true),
  1311.       "dc:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID],
  1312.                                          rssAuthor, true),
  1313.     },
  1314.  
  1315.     /********* ATOM 1.0 **********/
  1316.     "IN_ATOM": {
  1317.       "atom:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1318.                                      null, true),
  1319.       "atom:generator": new ElementInfo("generator", Cc[GENERATOR_CONTRACTID],
  1320.                                         atomGenerator, false),
  1321.       "atom:contributor": new ElementInfo("contributors",  Cc[PERSON_CONTRACTID],
  1322.                                           null, true),
  1323.       "atom:link": new ElementInfo("links", null, null, true),
  1324.       "atom:logo": new ElementInfo("atom:logo", null, atomLogo, false),
  1325.       "atom:entry": new ElementInfo("entries", Cc[ENTRY_CONTRACTID],
  1326.                                     null, true)
  1327.     },
  1328.  
  1329.     "IN_ENTRIES": {
  1330.       "atom:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1331.                                      null, true),
  1332.       "atom:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID],
  1333.                                           null, true),
  1334.       "atom:link": new ElementInfo("links", null, null, true),
  1335.     },
  1336.  
  1337.     /********* ATOM 0.3 **********/
  1338.     "IN_ATOM03": {
  1339.       "atom03:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1340.                                        null, true),
  1341.       "atom03:contributor": new ElementInfo("contributors",
  1342.                                             Cc[PERSON_CONTRACTID],
  1343.                                             null, true),
  1344.       "atom03:link": new ElementInfo("links", null, null, true),
  1345.       "atom03:entry": new ElementInfo("atom03_entries", Cc[ENTRY_CONTRACTID],
  1346.                                       null, true),
  1347.       "atom03:generator": new ElementInfo("generator", Cc[GENERATOR_CONTRACTID],
  1348.                                           atomGenerator, false),
  1349.     },
  1350.  
  1351.     "IN_ATOM03_ENTRIES": {
  1352.       "atom03:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1353.                                        null, true),
  1354.       "atom03:contributor": new ElementInfo("contributors",
  1355.                                             Cc[PERSON_CONTRACTID],
  1356.                                             null, true),
  1357.       "atom03:link": new ElementInfo("links", null, null, true),
  1358.       "atom03:entry": new ElementInfo("atom03_entries", Cc[ENTRY_CONTRACTID],
  1359.                                       null, true)
  1360.     }
  1361.   }
  1362. }
  1363.  
  1364. // See startElement for a long description of how feeds are processed.
  1365. FeedProcessor.prototype = { 
  1366.   
  1367.   // Set ourselves as the SAX handler, and set the base URI
  1368.   _init: function FP_init(uri) {
  1369.     this._reader.contentHandler = this;
  1370.     this._reader.errorHandler = this;
  1371.     this._result = Cc[FR_CONTRACTID].createInstance(Ci.nsIFeedResult);
  1372.     if (uri) {
  1373.       this._result.uri = uri;
  1374.       this._reader.baseURI = uri;
  1375.       this._xmlBaseStack[0] = uri;
  1376.     }
  1377.   },
  1378.  
  1379.   // This function is called once we figure out what type of feed
  1380.   // we're dealing with. Some feed types require digging a bit further
  1381.   // than the root.
  1382.   _docVerified: function FP_docVerified(version) {
  1383.     this._result.doc = Cc[FEED_CONTRACTID].createInstance(Ci.nsIFeed);
  1384.     this._result.doc.baseURI = 
  1385.       this._xmlBaseStack[this._xmlBaseStack.length - 1];
  1386.     this._result.doc.fields = this._feed;
  1387.     this._result.version = version;
  1388.   },
  1389.  
  1390.   // When we're done with the feed, let the listener know what
  1391.   // happened.
  1392.   _sendResult: function FP_sendResult() {
  1393.     this._haveSentResult = true;
  1394.     try {
  1395.       // Can be null when a non-feed is fed to us
  1396.       if (this._result.doc)
  1397.         this._result.doc.normalize();
  1398.     }
  1399.     catch (e) {
  1400.       LOG("FIXME: " + e);
  1401.     }
  1402.  
  1403.     try {
  1404.       if (this.listener != null)
  1405.         this.listener.handleResult(this._result);
  1406.     }
  1407.     finally {
  1408.       this._result = null;
  1409.     }
  1410.   },
  1411.  
  1412.   // Parsing functions
  1413.   parseFromStream: function FP_parseFromStream(stream, uri) {
  1414.     this._init(uri);
  1415.     this._reader.parseFromStream(stream, null, stream.available(), 
  1416.                                  "application/xml");
  1417.     this._reader = null;
  1418.   },
  1419.  
  1420.   parseFromString: function FP_parseFromString(inputString, uri) {
  1421.     this._init(uri);
  1422.     this._reader.parseFromString(inputString, "application/xml");
  1423.     this._reader = null;
  1424.   },
  1425.  
  1426.   parseAsync: function FP_parseAsync(requestObserver, uri) {
  1427.     this._init(uri);
  1428.     this._reader.parseAsync(requestObserver);
  1429.   },
  1430.  
  1431.   // nsIStreamListener 
  1432.  
  1433.   // The XMLReader will throw sensible exceptions if these get called
  1434.   // out of order.
  1435.   onStartRequest: function FP_onStartRequest(request, context) {
  1436.     // this will throw if the request is not a channel, but so will nsParser.
  1437.     var channel = request.QueryInterface(Ci.nsIChannel);
  1438.     channel.contentType = "application/vnd.mozilla.maybe.feed";
  1439.     this._reader.onStartRequest(request, context);
  1440.   },
  1441.  
  1442.   onStopRequest: function FP_onStopRequest(request, context, statusCode) {
  1443.     try {
  1444.       this._reader.onStopRequest(request, context, statusCode);
  1445.     }
  1446.     finally {
  1447.       this._reader = null;
  1448.     }
  1449.   },
  1450.  
  1451.   onDataAvailable:
  1452.   function FP_onDataAvailable(request, context, inputStream, offset, count) {
  1453.     this._reader.onDataAvailable(request, context, inputStream, offset, count);
  1454.   },
  1455.  
  1456.   // nsISAXErrorHandler
  1457.  
  1458.   // We only care about fatal errors. When this happens, we may have
  1459.   // parsed through the feed metadata and some number of entries. The
  1460.   // listener can still show some of that data if it wants, and we'll
  1461.   // set the bozo bit to indicate we were unable to parse all the way
  1462.   // through.
  1463.   fatalError: function FP_reportError() {
  1464.     this._result.bozo = true;
  1465.     //XXX need to QI to FeedProgressListener
  1466.     if (!this._haveSentResult)
  1467.       this._sendResult();
  1468.   },
  1469.  
  1470.   // nsISAXContentHandler
  1471.  
  1472.   startDocument: function FP_startDocument() {
  1473.     //LOG("----------");
  1474.   },
  1475.  
  1476.   endDocument: function FP_endDocument() {
  1477.     if (!this._haveSentResult)
  1478.       this._sendResult();
  1479.   },
  1480.  
  1481.   // The transitions defined above identify elements that contain more
  1482.   // than just text. For example RSS items contain many fields, and so
  1483.   // do Atom authors. The only commonly used elements that contain
  1484.   // mixed content are Atom Text Constructs of type="xhtml", which we
  1485.   // delegate to another handler for cleaning. That leaves a couple
  1486.   // different types of elements to deal with: those that should occur
  1487.   // only once, such as title elements, and those that can occur
  1488.   // multiple times, such as the RSS category element and the Atom
  1489.   // link element. Most of the RSS1/DC elements can occur multiple
  1490.   // times in theory, but in practice, the only ones that do have
  1491.   // analogues in Atom. 
  1492.   //
  1493.   // Some elements are also groups of attributes or sub-elements,
  1494.   // while others are simple text fields. For the most part, we don't
  1495.   // have to pay explicit attention to the simple text elements,
  1496.   // unless we want to post-process the resulting string to transform
  1497.   // it into some richer object like a Date or URI.
  1498.   //
  1499.   // Elements that have more sophisticated content models still end up
  1500.   // being dictionaries, whether they are based on attributes like RSS
  1501.   // cloud, sub-elements like Atom author, or even items and
  1502.   // entries. These elements are treated as "containers". It's
  1503.   // theoretically possible for a container to have an attribute with 
  1504.   // the same universal name as a sub-element, but none of the feed
  1505.   // formats allow this by default, and I don't of any extension that
  1506.   // works this way.
  1507.   //
  1508.   startElement: function FP_startElement(uri, localName, qName, attributes) {
  1509.     this._buf = "";
  1510.     ++this._depth;
  1511.     var elementInfo;
  1512.  
  1513.     //LOG("<" + localName + ">");
  1514.  
  1515.     // Check for xml:base
  1516.     var base = attributes.getValueFromName(XMLNS, "base");
  1517.     if (base) {
  1518.       this._xmlBaseStack[this._depth] =
  1519.         strToURI(base, this._xmlBaseStack[this._xmlBaseStack.length - 1]);
  1520.     }
  1521.  
  1522.     // To identify the element we're dealing with, we look up the
  1523.     // namespace URI in our gNamespaces dictionary, which will give us
  1524.     // a "canonical" prefix for a namespace URI. For example, this
  1525.     // allows Dublin Core "creator" elements to be consistently mapped
  1526.     // to "dc:creator", for easy field access by consumer code. This
  1527.     // strategy also happens to shorten up our state table.
  1528.     var key =  this._prefixForNS(uri) + localName;
  1529.  
  1530.     // Check to see if we need to hand this off to our XHTML handler.
  1531.     // The elements we're dealing with will look like this:
  1532.     // 
  1533.     // <title type="xhtml">
  1534.     //   <div xmlns="http://www.w3.org/1999/xhtml">
  1535.     //     A title with <b>bold</b> and <i>italics</i>.
  1536.     //   </div>
  1537.     // </title>
  1538.     //
  1539.     // When it returns in returnFromXHTMLHandler, the handler should
  1540.     // give us back a string like this: 
  1541.     // 
  1542.     //    "A title with <b>bold</b> and <i>italics</i>."
  1543.     //
  1544.     // The Atom spec explicitly says the div is not part of the content,
  1545.     // and explicitly allows whitespace collapsing.
  1546.     // 
  1547.     if ((this._result.version == "atom" || this._result.version == "atom03") &&
  1548.         this._textConstructs[key] != null) {
  1549.       var type = attributes.getValueFromName("","type");
  1550.       if (type != null && type.indexOf("xhtml") >= 0) {
  1551.         this._xhtmlHandler = 
  1552.           new XHTMLHandler(this, (this._result.version == "atom"), 
  1553.                            this._waiPrefixes);
  1554.         this._reader.contentHandler = this._xhtmlHandler;
  1555.         return;
  1556.       }
  1557.     }
  1558.  
  1559.     // Check our current state, and see if that state has a defined
  1560.     // transition. For example, this._trans["atom:entry"]["atom:author"]
  1561.     // will have one, and it tells us to add an item to our authors array.
  1562.     if (this._trans[this._state] && this._trans[this._state][key]) {
  1563.       elementInfo = this._trans[this._state][key];
  1564.     }
  1565.     else {
  1566.       // If we don't have a transition, hand off to extension handler
  1567.       this._extensionHandler = new ExtensionHandler(this);
  1568.       this._reader.contentHandler = this._extensionHandler;
  1569.       this._extensionHandler.startElement(uri, localName, qName, attributes);
  1570.       return;
  1571.     }
  1572.       
  1573.     // This distinguishes wrappers like 'channel' from elements
  1574.     // we'd actually like to do something with (which will test true).
  1575.     this._handlerStack[this._depth] = elementInfo; 
  1576.     if (elementInfo.isWrapper) {
  1577.       this._state = "IN_" + elementInfo.fieldName.toUpperCase();
  1578.       this._stack.push([this._feed, this._state]);
  1579.     } 
  1580.     else if (elementInfo.feedVersion) {
  1581.       this._state = "IN_" + elementInfo.fieldName.toUpperCase();
  1582.  
  1583.       // Check for the older RSS2 variants
  1584.       if (elementInfo.feedVersion == "rss2")
  1585.         elementInfo.feedVersion = this._findRSSVersion(attributes);
  1586.       else if (uri == RSS090NS)
  1587.         elementInfo.feedVersion = "rss090";
  1588.  
  1589.       this._docVerified(elementInfo.feedVersion);
  1590.       this._stack.push([this._feed, this._state]);
  1591.       this._mapAttributes(this._feed, attributes);
  1592.     }
  1593.     else {
  1594.       this._state = this._processComplexElement(elementInfo, attributes);
  1595.     }
  1596.   },
  1597.  
  1598.   // In the endElement handler, we decrement the stack and look
  1599.   // for cleanup/transition functions to execute. The second part
  1600.   // of the state transition works as above in startElement, but
  1601.   // the state we're looking for is prefixed with an underscore
  1602.   // to distinguish endElement events from startElement events.
  1603.   endElement:  function FP_endElement(uri, localName, qName) {
  1604.     var elementInfo = this._handlerStack[this._depth];
  1605.     //LOG("</" + localName + ">");
  1606.     if (elementInfo && !elementInfo.isWrapper)
  1607.       this._closeComplexElement(elementInfo);
  1608.   
  1609.     // cut down xml:base context
  1610.     if (this._xmlBaseStack.length == this._depth + 1)
  1611.       this._xmlBaseStack = this._xmlBaseStack.slice(0, this._depth);
  1612.  
  1613.     // our new state is whatever is at the top of the stack now
  1614.     if (this._stack.length > 0)
  1615.       this._state = this._stack[this._stack.length - 1][1];
  1616.     this._handlerStack = this._handlerStack.slice(0, this._depth);
  1617.     --this._depth;
  1618.   },
  1619.  
  1620.   // Buffer up character data. The buffer is cleared with every
  1621.   // opening element.
  1622.   characters: function FP_characters(data) {
  1623.     this._buf += data;
  1624.   },
  1625.   // TODO: It would be nice to check new prefixes here, and if they
  1626.   // don't conflict with the ones we've defined, throw them in a 
  1627.   // dictionary to check.
  1628.   startPrefixMapping: function FP_startPrefixMapping(prefix, uri) {
  1629.     // Thanks for QNames in content, W3C
  1630.     // This will even be a perf hit for every single feed
  1631.     // http://www.w3.org/WAI/PF/GUI/
  1632.     if (prefix && uri == WAIROLE_NS) 
  1633.       this._waiPrefixes[prefix] = WAIROLE_NS;
  1634.   },
  1635.   
  1636.   endPrefixMapping: function FP_endPrefixMapping(prefix) {
  1637.     if (prefix)
  1638.       delete this._waiPrefixes[prefix];
  1639.   },
  1640.   
  1641.   processingInstruction: function FP_processingInstruction(target, data) {
  1642.     if (target == "xml-stylesheet") {
  1643.       var hrefAttribute = data.match(/href=[\"\'](.*?)[\"\']/);
  1644.       if (hrefAttribute && hrefAttribute.length == 2) 
  1645.         this._result.stylesheet = strToURI(hrefAttribute[1], this._result.uri);
  1646.     }
  1647.   },
  1648.  
  1649.   // end of nsISAXContentHandler
  1650.  
  1651.   // Handle our more complicated elements--those that contain
  1652.   // attributes and child elements.
  1653.   _processComplexElement:
  1654.   function FP__processComplexElement(elementInfo, attributes) {
  1655.     var obj, key, prefix;
  1656.  
  1657.     // If the container is an entry/item, it'll need to have its 
  1658.     // more esoteric properties put in the 'fields' property bag.
  1659.     if (elementInfo.containerClass == Cc[ENTRY_CONTRACTID]) {
  1660.       obj = elementInfo.containerClass.createInstance(Ci.nsIFeedEntry);
  1661.       obj.baseURI = this._xmlBaseStack[this._xmlBaseStack.length - 1];
  1662.       this._mapAttributes(obj.fields, attributes);
  1663.     }
  1664.     else if (elementInfo.containerClass) {
  1665.       obj = elementInfo.containerClass.createInstance(Ci.nsIFeedElementBase);
  1666.       obj.baseURI = this._xmlBaseStack[this._xmlBaseStack.length - 1];
  1667.       obj.attributes = attributes; // just set the SAX attributes
  1668.     }
  1669.     else {
  1670.       obj = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2);
  1671.       this._mapAttributes(obj, attributes);
  1672.     }
  1673.  
  1674.     // We should have a container/propertyBag that's had its
  1675.     // attributes processed. Now we need to attach it to its
  1676.     // container.
  1677.     var newProp;
  1678.  
  1679.     // First we'll see what's on top of the stack.
  1680.     var container = this._stack[this._stack.length - 1][0];
  1681.  
  1682.     // Check to see if it has the property
  1683.     var prop;
  1684.     try {
  1685.       prop = container.getProperty(elementInfo.fieldName);
  1686.     }
  1687.     catch(e) {
  1688.     }
  1689.     
  1690.     if (elementInfo.isArray) {
  1691.       if (!prop) {
  1692.         container.setPropertyAsInterface(elementInfo.fieldName,
  1693.                                          Cc[ARRAY_CONTRACTID].
  1694.                                          createInstance(Ci.nsIMutableArray));
  1695.       }
  1696.  
  1697.       newProp = container.getProperty(elementInfo.fieldName);
  1698.       // XXX This QI should not be necessary, but XPConnect seems to fly
  1699.       // off the handle in the browser, and loses track of the interface
  1700.       // on large files. Bug 335638.
  1701.       newProp.QueryInterface(Ci.nsIMutableArray);
  1702.       newProp.appendElement(obj,false);
  1703.       
  1704.       // If new object is an nsIFeedContainer, we want to deal with
  1705.       // its member nsIPropertyBag instead.
  1706.       if (isIFeedContainer(obj))
  1707.         newProp = obj.fields; 
  1708.  
  1709.     }
  1710.     else {
  1711.       // If it doesn't, set it.
  1712.       if (!prop) {
  1713.         container.setPropertyAsInterface(elementInfo.fieldName,obj);
  1714.       }
  1715.       newProp = container.getProperty(elementInfo.fieldName);
  1716.     }
  1717.     
  1718.     // make our new state name, and push the property onto the stack
  1719.     var newState = "IN_" + elementInfo.fieldName.toUpperCase();
  1720.     this._stack.push([newProp, newState, obj]);
  1721.     return newState;
  1722.   },
  1723.  
  1724.   // Sometimes we need reconcile the element content with the object
  1725.   // model for a given feed. We use helper functions to do the
  1726.   // munging, but we need to identify array types here, so the munging
  1727.   // happens only to the last element of an array.
  1728.   _closeComplexElement: function FP__closeComplexElement(elementInfo) {
  1729.     var stateTuple = this._stack.pop();
  1730.     var container = stateTuple[0];
  1731.     var containerParent = stateTuple[2];
  1732.     var element = null;
  1733.     var isArray = isIArray(container);
  1734.  
  1735.     // If it's an array and we have to post-process,
  1736.     // grab the last element
  1737.     if (isArray)
  1738.       element = container.queryElementAt(container.length - 1, Ci.nsISupports);
  1739.     else
  1740.       element = container;
  1741.  
  1742.     // Run the post-processing function if there is one.
  1743.     if (elementInfo.closeFunc)
  1744.       element = elementInfo.closeFunc(this._buf, element);
  1745.  
  1746.     // If an nsIFeedContainer was on top of the stack,
  1747.     // we need to normalize it
  1748.     if (elementInfo.containerClass == Cc[ENTRY_CONTRACTID])
  1749.       containerParent.normalize();
  1750.  
  1751.     // If it's an array, re-set the last element
  1752.     if (isArray)
  1753.       container.replaceElementAt(element, container.length - 1, false);
  1754.   },
  1755.   
  1756.   _prefixForNS: function FP_prefixForNS(uri) {
  1757.     if (!uri)
  1758.       return "";
  1759.     var prefix = gNamespaces[uri];
  1760.     if (prefix)
  1761.       return prefix + ":";
  1762.     if (uri.toLowerCase().indexOf("http://backend.userland.com") == 0)
  1763.       return "";
  1764.     else
  1765.       return null;
  1766.   },
  1767.  
  1768.   _mapAttributes: function FP__mapAttributes(bag, attributes) {
  1769.     // Cycle through the attributes, and set our properties using the
  1770.     // prefix:localNames we find in our namespace dictionary.
  1771.     for (var i = 0; i < attributes.length; ++i) {
  1772.       var key = this._prefixForNS(attributes.getURI(i)) + attributes.getLocalName(i);
  1773.       var val = attributes.getValue(i);
  1774.       bag.setPropertyAsAString(key, val);
  1775.     }
  1776.   },
  1777.  
  1778.   // Only for RSS2esque formats
  1779.   _findRSSVersion: function FP__findRSSVersion(attributes) {
  1780.     var versionAttr = trimString(attributes.getValueFromName("", "version"));
  1781.     var versions = { "0.91":"rss091",
  1782.                      "0.92":"rss092",
  1783.                      "0.93":"rss093",
  1784.                      "0.94":"rss094" }
  1785.     if (versions[versionAttr])
  1786.       return versions[versionAttr];
  1787.     if (versionAttr.substr(0,2) != "2.")
  1788.       return "rssUnknown";
  1789.     return "rss2";
  1790.   },
  1791.  
  1792.   // unknown element values are returned here. See startElement above
  1793.   // for how this works.
  1794.   returnFromExtHandler:
  1795.   function FP_returnExt(uri, localName, chars, attributes) {
  1796.     --this._depth;
  1797.  
  1798.     // take control of the SAX events
  1799.     this._reader.contentHandler = this;
  1800.     if (localName == null && chars == null)
  1801.       return;
  1802.  
  1803.     // we don't take random elements inside rdf:RDF
  1804.     if (this._state == "IN_RDF")
  1805.       return;
  1806.     
  1807.     // Grab the top of the stack
  1808.     var top = this._stack[this._stack.length - 1];
  1809.     if (!top) 
  1810.       return;
  1811.  
  1812.     var container = top[0];
  1813.     // Grab the last element if it's an array
  1814.     if (isIArray(container)) {
  1815.       var contract = this._handlerStack[this._depth].containerClass;
  1816.       // check if it's something specific, but not an entry
  1817.       if (contract && contract != Cc[ENTRY_CONTRACTID]) {
  1818.         var el = container.queryElementAt(container.length - 1, 
  1819.                                           Ci.nsIFeedElementBase);
  1820.         // XXX there must be a way to flatten these interfaces
  1821.         if (contract == Cc[PERSON_CONTRACTID]) 
  1822.           el.QueryInterface(Ci.nsIFeedPerson);
  1823.         else
  1824.           return; // don't know about this interface
  1825.  
  1826.         var propName = localName;
  1827.         var prefix = gNamespaces[uri];
  1828.  
  1829.         // synonyms
  1830.         if ((uri == "" || 
  1831.              prefix &&
  1832.              ((prefix.indexOf("atom") > -1) ||
  1833.               (prefix.indexOf("rss") > -1))) && 
  1834.             (propName == "url" || propName == "href"))
  1835.           propName = "uri";
  1836.         
  1837.         try {
  1838.           if (el[propName] !== "undefined") {
  1839.             var propValue = chars;
  1840.             // convert URI-bearing values to an nsIURI
  1841.             if (propName == "uri") {
  1842.               var base = this._xmlBaseStack[this._xmlBaseStack.length - 1];
  1843.               propValue = strToURI(chars, base);
  1844.             }
  1845.             el[propName] = propValue;
  1846.           }
  1847.         }
  1848.         catch(e) {
  1849.           // ignore XPConnect errors
  1850.         }
  1851.         // the rest of the function deals with entry- and feed-level stuff
  1852.         return; 
  1853.       } 
  1854.       else {
  1855.         container = container.queryElementAt(container.length - 1, 
  1856.                                              Ci.nsIWritablePropertyBag2);
  1857.       }
  1858.     }
  1859.     
  1860.     // Make the buffer our new property
  1861.     var propName = this._prefixForNS(uri) + localName;
  1862.  
  1863.     // But, it could be something containing HTML. If so,
  1864.     // we need to know about that.
  1865.     if (this._textConstructs[propName] != null &&
  1866.         this._handlerStack[this._depth].containerClass !== null) {
  1867.       var newProp = Cc[TEXTCONSTRUCT_CONTRACTID].
  1868.                     createInstance(Ci.nsIFeedTextConstruct);
  1869.       newProp.text = chars;
  1870.       // Look up the default type in our table
  1871.       var type = this._textConstructs[propName];
  1872.       var typeAttribute = attributes.getValueFromName("","type");
  1873.       if (this._result.version == "atom" && typeAttribute != null) {
  1874.         type = typeAttribute;
  1875.       }
  1876.       else if (this._result.version == "atom03" && typeAttribute != null) {
  1877.         if (typeAttribute.toLowerCase().indexOf("xhtml") >= 0) {
  1878.           type = "xhtml";
  1879.         }
  1880.         else if (typeAttribute.toLowerCase().indexOf("html") >= 0) {
  1881.           type = "html";
  1882.         }
  1883.         else if (typeAttribute.toLowerCase().indexOf("text") >= 0) {
  1884.           type = "text";
  1885.         }
  1886.       }
  1887.       
  1888.       // If it's rss feed-level description, it's not supposed to have html
  1889.       if (this._result.version.indexOf("rss") >= 0 &&
  1890.           this._handlerStack[this._depth].containerClass != ENTRY_CONTRACTID) {
  1891.         type = "text";
  1892.       }
  1893.       newProp.type = type;
  1894.       newProp.base = this._xmlBaseStack[this._xmlBaseStack.length - 1];
  1895.       container.setPropertyAsInterface(propName, newProp);
  1896.     }
  1897.     else {
  1898.       container.setPropertyAsAString(propName, chars);
  1899.     }
  1900.   },
  1901.  
  1902.   // Sometimes, we'll hand off SAX handling duties to an XHTMLHandler
  1903.   // (see above) that will scrape out non-XHTML stuff, normalize
  1904.   // namespaces, and remove the wrapper div from Atom 1.0. When the
  1905.   // XHTMLHandler is done, it'll callback here.
  1906.   returnFromXHTMLHandler:
  1907.   function FP_returnFromXHTMLHandler(chars, uri, localName, qName) {
  1908.     // retake control of the SAX content events
  1909.     this._reader.contentHandler = this;
  1910.  
  1911.     // Grab the top of the stack
  1912.     var top = this._stack[this._stack.length - 1];
  1913.     if (!top) 
  1914.       return;
  1915.     var container = top[0];
  1916.  
  1917.     // Assign the property
  1918.     var newProp =  newProp = Cc[TEXTCONSTRUCT_CONTRACTID].
  1919.                    createInstance(Ci.nsIFeedTextConstruct);
  1920.     newProp.text = chars;
  1921.     newProp.type = "xhtml";
  1922.     newProp.base = this._xmlBaseStack[this._xmlBaseStack.length - 1];
  1923.     container.setPropertyAsInterface(this._prefixForNS(uri) + localName,
  1924.                                      newProp);
  1925.     
  1926.     // XHTML will cause us to peek too far. The XHTML handler will
  1927.     // send us an end element to call. RFC4287-valid feeds allow a
  1928.     // more graceful way to handle this. Unfortunately, we can't count
  1929.     // on compliance at this point.
  1930.     this.endElement(uri, localName, qName);
  1931.   },
  1932.  
  1933.   // XPCOM stuff
  1934.   classDescription: FP_CLASSNAME,
  1935.   classID: FP_CLASSID,
  1936.   contractID: FP_CONTRACTID,
  1937.   QueryInterface: XPCOMUtils.generateQI(
  1938.     [Ci.nsIFeedProcessor, Ci.nsISAXContentHandler, Ci.nsISAXErrorHandler,
  1939.      Ci.nsIStreamListener, Ci.nsIRequestObserver]
  1940.   )
  1941. }
  1942.  
  1943. var components = [FeedProcessor, FeedResult, Feed, Entry,
  1944.                   TextConstruct, Generator, Person];
  1945. function NSGetModule(compMgr, fileSpec) {
  1946.   return XPCOMUtils.generateModule(components);
  1947.   
  1948. }
  1949.