diff --git a/resource/feeds/FeedProcessor.js b/resource/feeds/FeedProcessor.js
new file mode 100644
index 0000000000..6daf8d70af
--- /dev/null
+++ b/resource/feeds/FeedProcessor.js
@@ -0,0 +1,1735 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function LOG(str) {
+ dump("*** " + str + "\n");
+}
+
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const FP_CONTRACTID = "@mozilla.org/feed-processor;1";
+const FP_CLASSID = Components.ID("{26acb1f0-28fc-43bc-867a-a46aabc85dd4}");
+const FP_CLASSNAME = "Feed Processor";
+const FR_CONTRACTID = "@mozilla.org/feed-result;1";
+const FR_CLASSID = Components.ID("{072a5c3d-30c6-4f07-b87f-9f63d51403f2}");
+const FR_CLASSNAME = "Feed Result";
+const FEED_CONTRACTID = "@mozilla.org/feed;1";
+const FEED_CLASSID = Components.ID("{5d0cfa97-69dd-4e5e-ac84-f253162e8f9a}");
+const FEED_CLASSNAME = "Feed";
+const ENTRY_CONTRACTID = "@mozilla.org/feed-entry;1";
+const ENTRY_CLASSID = Components.ID("{8e4444ff-8e99-4bdd-aa7f-fb3c1c77319f}");
+const ENTRY_CLASSNAME = "Feed Entry";
+const TEXTCONSTRUCT_CONTRACTID = "@mozilla.org/feed-textconstruct;1";
+const TEXTCONSTRUCT_CLASSID =
+ Components.ID("{b992ddcd-3899-4320-9909-924b3e72c922}");
+const TEXTCONSTRUCT_CLASSNAME = "Feed Text Construct";
+const GENERATOR_CONTRACTID = "@mozilla.org/feed-generator;1";
+const GENERATOR_CLASSID =
+ Components.ID("{414af362-9ad8-4296-898e-62247f25a20e}");
+const GENERATOR_CLASSNAME = "Feed Generator";
+const PERSON_CONTRACTID = "@mozilla.org/feed-person;1";
+const PERSON_CLASSID = Components.ID("{95c963b7-20b2-11db-92f6-001422106990}");
+const PERSON_CLASSNAME = "Feed Person";
+
+const IO_CONTRACTID = "@mozilla.org/network/io-service;1";
+const BAG_CONTRACTID = "@mozilla.org/hash-property-bag;1";
+const ARRAY_CONTRACTID = "@mozilla.org/array;1";
+const SAX_CONTRACTID = "@mozilla.org/saxparser/xmlreader;1";
+const PARSERUTILS_CONTRACTID = "@mozilla.org/parserutils;1";
+
+const gMimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+
+const XMLNS = "http://www.w3.org/XML/1998/namespace";
+const RSS090NS = "http://my.netscape.com/rdf/simple/0.9/";
+
+/** *** Some general utils *****/
+function strToURI(link, base) {
+ base = base || null;
+ try {
+ return Services.io.newURI(link, null, base);
+ } catch (e) {
+ return null;
+ }
+}
+
+function isArray(a) {
+ return isObject(a) && a.constructor == Array;
+}
+
+function isObject(a) {
+ return (a && typeof a == "object") || isFunction(a);
+}
+
+function isFunction(a) {
+ return typeof a == "function";
+}
+
+function isIID(a, iid) {
+ var rv = false;
+ try {
+ a.QueryInterface(iid);
+ rv = true;
+ } catch (e) {
+ }
+ return rv;
+}
+
+function isIArray(a) {
+ return isIID(a, Ci.nsIArray);
+}
+
+function isIFeedContainer(a) {
+ return isIID(a, Ci.nsIFeedContainer);
+}
+
+function stripTags(someHTML) {
+ return someHTML.replace(/<[^>]+>/g, "");
+}
+
+/**
+ * Searches through an array of links and returns a JS array
+ * of matching property bags.
+ */
+const IANA_URI = "http://www.iana.org/assignments/relation/";
+function findAtomLinks(rel, links) {
+ var rvLinks = [];
+ for (var i = 0; i < links.length; ++i) {
+ var linkElement = links.queryElementAt(i, Ci.nsIPropertyBag2);
+ // atom:link MUST have @href
+ if (bagHasKey(linkElement, "href")) {
+ var relAttribute = null;
+ if (bagHasKey(linkElement, "rel"))
+ relAttribute = linkElement.getPropertyAsAString("rel");
+ if ((!relAttribute && rel == "alternate") || relAttribute == rel) {
+ rvLinks.push(linkElement);
+ continue;
+ }
+ // catch relations specified by IANA URI
+ if (relAttribute == IANA_URI + rel) {
+ rvLinks.push(linkElement);
+ }
+ }
+ }
+ return rvLinks;
+}
+
+function xmlEscape(s) {
+ s = s.replace(/&/g, "&");
+ s = s.replace(/>/g, ">");
+ s = s.replace(/ 0) {
+ ++entries_with_enclosures;
+
+ for (var e = 0; e < entry.enclosures.length; ++e) {
+ var enc = entry.enclosures.queryElementAt(e, Ci.nsIWritablePropertyBag2);
+ if (enc.hasKey("type")) {
+ var enctype = enc.get("type");
+
+ if (/^audio/.test(enctype)) {
+ ++audio_count;
+ } else if (/^image/.test(enctype)) {
+ ++image_count;
+ } else if (/^video/.test(enctype)) {
+ ++video_count;
+ } else {
+ ++other_count;
+ }
+ } else {
+ ++other_count;
+ }
+ }
+ }
+ }
+
+ var feedtype = Ci.nsIFeed.TYPE_FEED;
+
+ // For a feed to be marked as TYPE_VIDEO, TYPE_AUDIO and TYPE_IMAGE,
+ // we enforce two things:
+ //
+ // 1. all entries must have at least one enclosure
+ // 2. all enclosures must be video for TYPE_VIDEO, audio for TYPE_AUDIO or image
+ // for TYPE_IMAGE
+ //
+ // Otherwise it's a TYPE_FEED.
+ if (entries_with_enclosures == this.items.length && other_count == 0) {
+ if (audio_count > 0 && !video_count && !image_count) {
+ feedtype = Ci.nsIFeed.TYPE_AUDIO;
+
+ } else if (image_count > 0 && !audio_count && !video_count) {
+ feedtype = Ci.nsIFeed.TYPE_IMAGE;
+
+ } else if (video_count > 0 && !audio_count && !image_count) {
+ feedtype = Ci.nsIFeed.TYPE_VIDEO;
+ }
+ }
+
+ this.type = feedtype;
+ this.enclosureCount = other_count + video_count + audio_count + image_count;
+ },
+
+ _atomLinksToURI: function Feed_linkToURI() {
+ var links = this.fields.getPropertyAsInterface("links", Ci.nsIArray);
+ var alternates = findAtomLinks("alternate", links);
+ if (alternates.length > 0) {
+ var href = alternates[0].getPropertyAsAString("href");
+ var base;
+ if (bagHasKey(alternates[0], "xml:base"))
+ base = alternates[0].getPropertyAsAString("xml:base");
+ this.link = this._resolveURI(href, base);
+ }
+ },
+
+ _resolveImageLink: function Feed_resolveImageLink() {
+ var base;
+ if (bagHasKey(this.image, "xml:base"))
+ base = this.image.getPropertyAsAString("xml:base");
+ var url = this._resolveURI(this.image.getPropertyAsAString("url"), base);
+ if (url)
+ this.image.setPropertyAsAString("url", url.spec);
+ },
+
+ _resolveURI: function Feed_resolveURI(linkSpec, baseSpec) {
+ var uri = null;
+ try {
+ var base = baseSpec ? strToURI(baseSpec, this.baseURI) : this.baseURI;
+ uri = strToURI(linkSpec, base);
+ } catch (e) {
+ LOG(e);
+ }
+
+ return uri;
+ },
+
+ // reset the bag to raw contents, not text constructs
+ _resetBagMembersToRawText: function Feed_resetBagMembers(fieldLists) {
+ for (var i = 0; i < fieldLists.length; i++) {
+ for (var j = 0; j < fieldLists[i].length; j++) {
+ if (bagHasKey(this.fields, fieldLists[i][j])) {
+ var textConstruct = this.fields.getProperty(fieldLists[i][j]);
+ this.fields.setPropertyAsAString(fieldLists[i][j],
+ textConstruct.text);
+ }
+ }
+ }
+ },
+
+ // XPCOM stuff
+ classID: FEED_CLASSID,
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIFeed, Ci.nsIFeedContainer]),
+};
+
+function Entry() {
+ this.summary = null;
+ this.content = null;
+ this.title = null;
+ this.fields = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag2);
+ this.link = null;
+ this.id = null;
+ this.baseURI = null;
+ this.updated = null;
+ this.published = null;
+ this.authors = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray);
+ this.contributors = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray);
+}
+
+Entry.prototype = {
+ fields: null,
+ enclosures: null,
+ mediaContent: null,
+
+ searchLists: {
+ title: ["title", "rss1:title", "atom03:title", "atom:title"],
+ link: [["link", strToURI], ["rss1:link", strToURI]],
+ id: [["guid", makePropGetter("guid")], "rdf:about",
+ "atom03:id", "atom:id"],
+ authors: ["authors"],
+ contributors: ["contributors"],
+ summary: ["description", "rss1:description", "dc:description",
+ "atom03:summary", "atom:summary"],
+ content: ["content:encoded", "atom03:content", "atom:content"],
+ rights: ["atom03:rights", "atom:rights"],
+ published: ["pubDate", "atom03:issued", "dcterms:issued", "atom:published"],
+ updated: ["pubDate", "atom03:modified", "dc:date", "dcterms:modified",
+ "atom:updated"],
+ },
+
+ normalize: function Entry_normalize() {
+ fieldsToObj(this, this.searchLists);
+
+ // Assign Atom link if needed
+ if (bagHasKey(this.fields, "links"))
+ this._atomLinksToURI();
+
+ // Populate enclosures array
+ this._populateEnclosures();
+
+ // The link might be a guid w/ permalink=true
+ if (!this.link && bagHasKey(this.fields, "guid")) {
+ var guid = this.fields.getProperty("guid");
+ var isPermaLink = true;
+
+ if (bagHasKey(guid, "isPermaLink"))
+ isPermaLink = guid.getProperty("isPermaLink").toLowerCase() != "false";
+
+ if (guid && isPermaLink)
+ this.link = strToURI(guid.getProperty("guid"));
+ }
+
+ if (this.updated)
+ this.updated = dateParse(this.updated);
+ if (this.published)
+ this.published = dateParse(this.published);
+
+ this._resetBagMembersToRawText([this.searchLists.content,
+ this.searchLists.summary,
+ this.searchLists.title]);
+ },
+
+ _populateEnclosures: function Entry_populateEnclosures() {
+ if (bagHasKey(this.fields, "links"))
+ this._atomLinksToEnclosures();
+
+ // Add RSS2 enclosure to enclosures
+ if (bagHasKey(this.fields, "enclosure"))
+ this._enclosureToEnclosures();
+
+ // Add media:content to enclosures
+ if (bagHasKey(this.fields, "mediacontent"))
+ this._mediaToEnclosures("mediacontent");
+
+ // Add media:thumbnail to enclosures
+ if (bagHasKey(this.fields, "mediathumbnail"))
+ this._mediaToEnclosures("mediathumbnail");
+
+ // Add media:content in media:group to enclosures
+ if (bagHasKey(this.fields, "mediagroup"))
+ this._mediaToEnclosures("mediagroup", "mediacontent");
+ },
+
+ __enclosure_map: null,
+
+ _addToEnclosures: function Entry_addToEnclosures(new_enc) {
+ // items we add to the enclosures array get displayed in the FeedWriter and
+ // they must have non-empty urls.
+ if (!bagHasKey(new_enc, "url") || new_enc.getPropertyAsAString("url") == "")
+ return;
+
+ if (this.__enclosure_map == null)
+ this.__enclosure_map = {};
+
+ var previous_enc = this.__enclosure_map[new_enc.getPropertyAsAString("url")];
+
+ if (previous_enc != undefined) {
+ previous_enc.QueryInterface(Ci.nsIWritablePropertyBag2);
+
+ if (!bagHasKey(previous_enc, "type") && bagHasKey(new_enc, "type")) {
+ previous_enc.setPropertyAsAString("type", new_enc.getPropertyAsAString("type"));
+ try {
+ let handlerInfoWrapper = gMimeService.getFromTypeAndExtension(new_enc.getPropertyAsAString("type"), null);
+ if (handlerInfoWrapper && handlerInfoWrapper.description) {
+ previous_enc.setPropertyAsAString("typeDesc", handlerInfoWrapper.description);
+ }
+ } catch (ext) {}
+ }
+
+ if (!bagHasKey(previous_enc, "length") && bagHasKey(new_enc, "length"))
+ previous_enc.setPropertyAsAString("length", new_enc.getPropertyAsAString("length"));
+
+ return;
+ }
+
+ if (this.enclosures == null) {
+ this.enclosures = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray);
+ this.enclosures.QueryInterface(Ci.nsIMutableArray);
+ }
+
+ this.enclosures.appendElement(new_enc);
+ this.__enclosure_map[new_enc.getPropertyAsAString("url")] = new_enc;
+ },
+
+ _atomLinksToEnclosures: function Entry_linkToEnclosure() {
+ var links = this.fields.getPropertyAsInterface("links", Ci.nsIArray);
+ var enc_links = findAtomLinks("enclosure", links);
+ if (enc_links.length == 0)
+ return;
+
+ for (var i = 0; i < enc_links.length; ++i) {
+ var link = enc_links[i];
+
+ // an enclosure must have an href
+ if (!(link.getProperty("href")))
+ return;
+
+ var enc = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2);
+
+ // copy Atom bits over to equivalent enclosure bits
+ enc.setPropertyAsAString("url", link.getPropertyAsAString("href"));
+ if (bagHasKey(link, "type"))
+ enc.setPropertyAsAString("type", link.getPropertyAsAString("type"));
+ if (bagHasKey(link, "length"))
+ enc.setPropertyAsAString("length", link.getPropertyAsAString("length"));
+
+ this._addToEnclosures(enc);
+ }
+ },
+
+ _enclosureToEnclosures: function Entry_enclosureToEnclosures() {
+ var enc = this.fields.getPropertyAsInterface("enclosure", Ci.nsIPropertyBag2);
+
+ if (!(enc.getProperty("url")))
+ return;
+
+ this._addToEnclosures(enc);
+ },
+
+ _mediaToEnclosures: function Entry_mediaToEnclosures(mediaType, contentType) {
+ var content;
+
+ // If a contentType is specified, the mediaType is a simple propertybag,
+ // and the contentType is an array inside it.
+ if (contentType) {
+ var group = this.fields.getPropertyAsInterface(mediaType, Ci.nsIPropertyBag2);
+ content = group.getPropertyAsInterface(contentType, Ci.nsIArray);
+ } else {
+ content = this.fields.getPropertyAsInterface(mediaType, Ci.nsIArray);
+ }
+
+ for (var i = 0; i < content.length; ++i) {
+ var contentElement = content.queryElementAt(i, Ci.nsIWritablePropertyBag2);
+
+ // media:content don't require url, but if it's not there, we should
+ // skip it.
+ if (!bagHasKey(contentElement, "url"))
+ continue;
+
+ var enc = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2);
+
+ // copy media:content bits over to equivalent enclosure bits
+ enc.setPropertyAsAString("url", contentElement.getPropertyAsAString("url"));
+ if (bagHasKey(contentElement, "type")) {
+ enc.setPropertyAsAString("type", contentElement.getPropertyAsAString("type"));
+ } else if (mediaType == "mediathumbnail") {
+ // thumbnails won't have a type, but default to image types
+ enc.setPropertyAsAString("type", "image/*");
+ enc.setPropertyAsBool("thumbnail", true);
+ }
+
+ if (bagHasKey(contentElement, "fileSize")) {
+ enc.setPropertyAsAString("length", contentElement.getPropertyAsAString("fileSize"));
+ }
+
+ this._addToEnclosures(enc);
+ }
+ },
+
+ // XPCOM stuff
+ classID: ENTRY_CLASSID,
+ QueryInterface: ChromeUtils.generateQI(
+ [Ci.nsIFeedEntry, Ci.nsIFeedContainer]
+ ),
+};
+
+Entry.prototype._atomLinksToURI = Feed.prototype._atomLinksToURI;
+Entry.prototype._resolveURI = Feed.prototype._resolveURI;
+Entry.prototype._resetBagMembersToRawText =
+ Feed.prototype._resetBagMembersToRawText;
+
+// TextConstruct represents and element that could contain (X)HTML
+function TextConstruct() {
+ this.lang = null;
+ this.base = null;
+ this.type = "text";
+ this.text = null;
+ this.parserUtils = Cc[PARSERUTILS_CONTRACTID].getService(Ci.nsIParserUtils);
+}
+
+TextConstruct.prototype = {
+ plainText: function TC_plainText() {
+ if (this.type != "text") {
+ return this.parserUtils.convertToPlainText(stripTags(this.text),
+ Ci.nsIDocumentEncoder.OutputSelectionOnly |
+ Ci.nsIDocumentEncoder.OutputAbsoluteLinks,
+ 0);
+ }
+ return this.text;
+ },
+
+ createDocumentFragment: function TC_createDocumentFragment(element) {
+ if (this.type == "text") {
+ var doc = element.ownerDocument;
+ var docFragment = doc.createDocumentFragment();
+ var node = doc.createTextNode(this.text);
+ docFragment.appendChild(node);
+ return docFragment;
+ }
+ var isXML;
+ if (this.type == "xhtml")
+ isXML = true;
+ else if (this.type == "html")
+ isXML = false;
+ else
+ return null;
+
+ let flags = Ci.nsIParserUtils.SanitizerDropForms;
+ return this.parserUtils.parseFragment(this.text, flags, isXML,
+ this.base, element);
+ },
+
+ // XPCOM stuff
+ classID: TEXTCONSTRUCT_CLASSID,
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIFeedTextConstruct]),
+};
+
+// Generator represents the software that produced the feed
+function Generator() {
+ this.lang = null;
+ this.agent = null;
+ this.version = null;
+ this.uri = null;
+
+ // nsIFeedElementBase
+ this._attributes = null;
+ this.baseURI = null;
+}
+
+Generator.prototype = {
+
+ get attributes() {
+ return this._attributes;
+ },
+
+ set attributes(value) {
+ this._attributes = value;
+ this.version = this._attributes.getValueFromName("", "version");
+ var uriAttribute = this._attributes.getValueFromName("", "uri") ||
+ this._attributes.getValueFromName("", "url");
+ this.uri = strToURI(uriAttribute, this.baseURI);
+
+ // RSS1
+ uriAttribute = this._attributes.getValueFromName(RDF_NS, "resource");
+ if (uriAttribute) {
+ this.agent = uriAttribute;
+ this.uri = strToURI(uriAttribute, this.baseURI);
+ }
+ },
+
+ // XPCOM stuff
+ classID: GENERATOR_CLASSID,
+ QueryInterface: ChromeUtils.generateQI(
+ [Ci.nsIFeedGenerator, Ci.nsIFeedElementBase]
+ ),
+};
+
+function Person() {
+ this.name = null;
+ this.uri = null;
+ this.email = null;
+
+ // nsIFeedElementBase
+ this.attributes = null;
+ this.baseURI = null;
+}
+
+Person.prototype = {
+ // XPCOM stuff
+ classID: PERSON_CLASSID,
+ QueryInterface: ChromeUtils.generateQI(
+ [Ci.nsIFeedPerson, Ci.nsIFeedElementBase]
+ ),
+};
+
+/**
+ * Map a list of fields into properties on a container.
+ *
+ * @param container An nsIFeedContainer
+ * @param fields A list of fields to search for. List members can
+ * be a list, in which case the second member is
+ * transformation function (like parseInt).
+ */
+function fieldsToObj(container, fields) {
+ var props, prop, field, searchList;
+ for (var key in fields) {
+ searchList = fields[key];
+ for (var i = 0; i < searchList.length; ++i) {
+ props = searchList[i];
+ prop = null;
+ field = isArray(props) ? props[0] : props;
+ try {
+ prop = container.fields.getProperty(field);
+ } catch (e) {
+ }
+ if (prop) {
+ prop = isArray(props) ? props[1](prop) : prop;
+ container[key] = prop;
+ }
+ }
+ }
+}
+
+/**
+ * Lower cases an element's localName property
+ * @param element A DOM element.
+ *
+ * @returns The lower case localName property of the specified element
+ */
+function LC(element) {
+ return element.localName.toLowerCase();
+}
+
+// TODO move these post-processor functions
+// create a generator element
+function atomGenerator(s, generator) {
+ generator.QueryInterface(Ci.nsIFeedGenerator);
+ generator.agent = s.trim();
+ return generator;
+}
+
+// post-process atom:logo to create an RSS2-like structure
+function atomLogo(s, logo) {
+ logo.setPropertyAsAString("url", s.trim());
+}
+
+// post-process an RSS category, map it to the Atom fields.
+function rssCatTerm(s, cat) {
+ // add slash handling?
+ cat.setPropertyAsAString("term", s.trim());
+ return cat;
+}
+
+// post-process a GUID
+function rssGuid(s, guid) {
+ guid.setPropertyAsAString("guid", s.trim());
+ return guid;
+}
+
+// post-process an RSS author element
+//
+// It can contain a field like this:
+//
+// lawyer@boyer.net (Lawyer Boyer)
+//
+// or, delightfully, a field like this:
+//
+// Simon St.Laurent (mailto:simonstl@simonstl.com)
+//
+// We want to split this up and assign it to corresponding Atom
+// fields.
+//
+function rssAuthor(s, author) {
+ author.QueryInterface(Ci.nsIFeedPerson);
+ // check for RSS2 string format
+ var chars = s.trim();
+ var matches = chars.match(/(.*)\((.*)\)/);
+ var emailCheck =
+ /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
+ if (matches) {
+ var match1 = matches[1].trim();
+ var match2 = matches[2].trim();
+ if (match2.indexOf("mailto:") == 0)
+ match2 = match2.substring(7);
+ if (emailCheck.test(match1)) {
+ author.email = match1;
+ author.name = match2;
+ } else if (emailCheck.test(match2)) {
+ author.email = match2;
+ author.name = match1;
+ } else {
+ // put it back together
+ author.name = match1 + " (" + match2 + ")";
+ }
+ } else {
+ author.name = chars;
+ if (chars.indexOf("@"))
+ author.email = chars;
+ }
+ return author;
+}
+
+//
+// skipHours and skipDays map to arrays, so we need to change the
+// string to an nsISupports in order to stick it in there.
+//
+function rssArrayElement(s) {
+ var str = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ str.data = s;
+ str.QueryInterface(Ci.nsISupportsString);
+ return str;
+}
+
+/**
+ * Tries parsing a string through the JavaScript Date object.
+ * @param aDateString
+ * A string that is supposedly an RFC822 or RFC3339 date.
+ * @return A Date.toUTCString, or null if the string can't be parsed.
+ */
+function dateParse(aDateString) {
+ let dateString = aDateString.trim();
+ // Without bug 682781 fixed, JS won't parse an RFC822 date with a Z for the
+ // timezone, so convert to -00:00 which works for any date format.
+ dateString = dateString.replace(/z$/i, "-00:00");
+ let date = new Date(dateString);
+ if (!isNaN(date)) {
+ return date.toUTCString();
+ }
+ return null;
+}
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+// The XHTMLHandler handles inline XHTML found in things like atom:summary
+function XHTMLHandler(processor, isAtom) {
+ this._buf = "";
+ this._processor = processor;
+ this._depth = 0;
+ this._isAtom = isAtom;
+ // a stack of lists tracking in-scope namespaces
+ this._inScopeNS = [];
+}
+
+// The fidelity can be improved here, to allow handling of stuff like
+// SVG and MathML. XXX
+XHTMLHandler.prototype = {
+
+ // look back up at the declared namespaces
+ // we always use the same prefixes for our safe stuff
+ _isInScope: function XH__isInScope(ns) {
+ for (var i in this._inScopeNS) {
+ for (var uri in this._inScopeNS[i]) {
+ if (this._inScopeNS[i][uri] == ns)
+ return true;
+ }
+ }
+ return false;
+ },
+
+ startDocument: function XH_startDocument() {
+ },
+ endDocument: function XH_endDocument() {
+ },
+ startElement: function XH_startElement(namespace, localName, qName, attributes) {
+ ++this._depth;
+ this._inScopeNS.push([]);
+
+ // RFC4287 requires XHTML to be wrapped in a div that is *not* part of
+ // the content. This prevents people from screwing up namespaces, but
+ // we need to skip it here.
+ if (this._isAtom && this._depth == 1 && localName == "div")
+ return;
+
+ // If it's an XHTML element, record it. Otherwise, it's ignored.
+ if (namespace == XHTML_NS) {
+ this._buf += "<" + localName;
+ var uri;
+ for (var i = 0; i < attributes.length; ++i) {
+ uri = attributes.getURI(i);
+ // XHTML attributes aren't in a namespace
+ if (uri == "") {
+ this._buf += (" " + attributes.getLocalName(i) + "='" +
+ xmlEscape(attributes.getValue(i)) + "'");
+ } else {
+ // write a small set of allowed attribute namespaces
+ var prefix = gAllowedXHTMLNamespaces[uri];
+ if (prefix != null) {
+ // The attribute value we'll attempt to write
+ var attributeValue = xmlEscape(attributes.getValue(i));
+
+ // it's an allowed attribute NS.
+ // write the attribute
+ this._buf += (" " + prefix + ":" +
+ attributes.getLocalName(i) +
+ "='" + attributeValue + "'");
+
+ // write an xmlns declaration if necessary
+ if (prefix != "xml" && !this._isInScope(uri)) {
+ this._inScopeNS[this._inScopeNS.length - 1].push(uri);
+ this._buf += " xmlns:" + prefix + "='" + uri + "'";
+ }
+ }
+ }
+ }
+ this._buf += ">";
+ }
+ },
+ endElement: function XH_endElement(uri, localName, qName) {
+ --this._depth;
+ this._inScopeNS.pop();
+
+ // We need to skip outer divs in Atom. See comment in startElement.
+ if (this._isAtom && this._depth == 0 && localName == "div")
+ return;
+
+ // When we peek too far, go back to the main processor
+ if (this._depth < 0) {
+ this._processor.returnFromXHTMLHandler(this._buf.trim(),
+ uri, localName, qName);
+ return;
+ }
+ // If it's an XHTML element, record it. Otherwise, it's ignored.
+ if (uri == XHTML_NS) {
+ this._buf += "" + localName + ">";
+ }
+ },
+ characters: function XH_characters(data) {
+ this._buf += xmlEscape(data);
+ },
+ processingInstruction: function XH_processingInstruction() {
+ },
+};
+
+/**
+ * The ExtensionHandler deals with elements we haven't explicitly
+ * added to our transition table in the FeedProcessor.
+ */
+function ExtensionHandler(processor) {
+ this._buf = "";
+ this._depth = 0;
+ this._hasChildElements = false;
+
+ // The FeedProcessor
+ this._processor = processor;
+
+ // Fields of the outermost extension element.
+ this._localName = null;
+ this._uri = null;
+ this._qName = null;
+ this._attrs = null;
+}
+
+ExtensionHandler.prototype = {
+ startDocument: function EH_startDocument() {
+ },
+ endDocument: function EH_endDocument() {
+ },
+ startElement: function EH_startElement(uri, localName, qName, attrs) {
+ ++this._depth;
+
+ if (this._depth == 1) {
+ this._uri = uri;
+ this._localName = localName;
+ this._qName = qName;
+ this._attrs = attrs;
+ }
+
+ // if we descend into another element, we won't send text
+ this._hasChildElements = (this._depth > 1);
+
+ },
+ endElement: function EH_endElement(uri, localName, qName) {
+ --this._depth;
+ if (this._depth == 0) {
+ var text = this._hasChildElements ? null : this._buf.trim();
+ this._processor.returnFromExtHandler(this._uri, this._localName,
+ text, this._attrs);
+ }
+ },
+ characters: function EH_characters(data) {
+ if (!this._hasChildElements)
+ this._buf += data;
+ },
+ processingInstruction: function EH_processingInstruction() {
+ },
+};
+
+
+/**
+ * ElementInfo is a simple container object that describes
+ * some characteristics of a feed element. For example, it
+ * says whether an element can be expected to appear more
+ * than once inside a given entry or feed.
+ */
+function ElementInfo(fieldName, containerClass, closeFunc, isArray) {
+ this.fieldName = fieldName;
+ this.containerClass = containerClass;
+ this.closeFunc = closeFunc;
+ this.isArray = isArray;
+ this.isWrapper = false;
+}
+
+/**
+ * FeedElementInfo represents a feed element, usually the root.
+ */
+function FeedElementInfo(fieldName, feedVersion) {
+ this.isWrapper = false;
+ this.fieldName = fieldName;
+ this.feedVersion = feedVersion;
+}
+
+/**
+ * Some feed formats include vestigial wrapper elements that we don't
+ * want to include in our object model, but we do need to keep track
+ * of during parsing.
+ */
+function WrapperElementInfo(fieldName) {
+ this.isWrapper = true;
+ this.fieldName = fieldName;
+}
+
+/** *** The Processor *****/
+function FeedProcessor() {
+ this._reader = Cc[SAX_CONTRACTID].createInstance(Ci.nsISAXXMLReader);
+ this._buf = "";
+ this._feed = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2);
+ this._handlerStack = [];
+ this._xmlBaseStack = []; // sparse array keyed to nesting depth
+ this._depth = 0;
+ this._state = "START";
+ this._result = null;
+ this._extensionHandler = null;
+ this._xhtmlHandler = null;
+ this._haveSentResult = false;
+
+ // The nsIFeedResultListener waiting for the parse results
+ this.listener = null;
+
+ // These elements can contain (X)HTML or plain text.
+ // We keep a table here that contains their default treatment
+ this._textConstructs = {"atom:title": "text",
+ "atom:summary": "text",
+ "atom:rights": "text",
+ "atom:content": "text",
+ "atom:subtitle": "text",
+ "description": "html",
+ "rss1:description": "html",
+ "dc:description": "html",
+ "content:encoded": "html",
+ "title": "text",
+ "rss1:title": "text",
+ "atom03:title": "text",
+ "atom03:tagline": "text",
+ "atom03:summary": "text",
+ "atom03:content": "text"};
+ this._stack = [];
+
+ this._trans = {
+ "START": {
+ // If we hit a root RSS element, treat as RSS2.
+ "rss": new FeedElementInfo("RSS2", "rss2"),
+
+ // If we hit an RDF element, if could be RSS1, but we can't
+ // verify that until we hit a rss1:channel element.
+ "rdf:RDF": new WrapperElementInfo("RDF"),
+
+ // If we hit a Atom 1.0 element, treat as Atom 1.0.
+ "atom:feed": new FeedElementInfo("Atom", "atom"),
+
+ // Treat as Atom 0.3
+ "atom03:feed": new FeedElementInfo("Atom03", "atom03"),
+ },
+
+ /** ******* RSS2 **********/
+ "IN_RSS2": {
+ "channel": new WrapperElementInfo("channel"),
+ },
+
+ "IN_CHANNEL": {
+ "item": new ElementInfo("items", Cc[ENTRY_CONTRACTID], null, true),
+ "managingEditor": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ "dc:creator": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ "dc:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ "dc:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ "category": new ElementInfo("categories", null, rssCatTerm, true),
+ "cloud": new ElementInfo("cloud", null, null, false),
+ "image": new ElementInfo("image", null, null, false),
+ "textInput": new ElementInfo("textInput", null, null, false),
+ "skipDays": new ElementInfo("skipDays", null, null, false),
+ "skipHours": new ElementInfo("skipHours", null, null, false),
+ "generator": new ElementInfo("generator", Cc[GENERATOR_CONTRACTID],
+ atomGenerator, false),
+ },
+
+ "IN_ITEMS": {
+ "author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ "dc:creator": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ "dc:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ "dc:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ "category": new ElementInfo("categories", null, rssCatTerm, true),
+ "enclosure": new ElementInfo("enclosure", null, null, false),
+ "media:content": new ElementInfo("mediacontent", null, null, true),
+ "media:group": new ElementInfo("mediagroup", null, null, false),
+ "media:thumbnail": new ElementInfo("mediathumbnail", null, null, true),
+ "guid": new ElementInfo("guid", null, rssGuid, false),
+ },
+
+ "IN_SKIPDAYS": {
+ "day": new ElementInfo("days", null, rssArrayElement, true),
+ },
+
+ "IN_SKIPHOURS": {
+ "hour": new ElementInfo("hours", null, rssArrayElement, true),
+ },
+
+ "IN_MEDIAGROUP": {
+ "media:content": new ElementInfo("mediacontent", null, null, true),
+ "media:thumbnail": new ElementInfo("mediathumbnail", null, null, true),
+ },
+
+ /** ******* RSS1 **********/
+ "IN_RDF": {
+ // If we hit a rss1:channel, we can verify that we have RSS1
+ "rss1:channel": new FeedElementInfo("rdf_channel", "rss1"),
+ "rss1:image": new ElementInfo("image", null, null, false),
+ "rss1:textinput": new ElementInfo("textInput", null, null, false),
+ "rss1:item": new ElementInfo("items", Cc[ENTRY_CONTRACTID], null, true),
+ },
+
+ "IN_RDF_CHANNEL": {
+ "admin:generatorAgent": new ElementInfo("generator",
+ Cc[GENERATOR_CONTRACTID],
+ null, false),
+ "dc:creator": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ "dc:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ "dc:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ },
+
+ /** ******* ATOM 1.0 **********/
+ "IN_ATOM": {
+ "atom:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ null, true),
+ "atom:generator": new ElementInfo("generator", Cc[GENERATOR_CONTRACTID],
+ atomGenerator, false),
+ "atom:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID],
+ null, true),
+ "atom:link": new ElementInfo("links", null, null, true),
+ "atom:logo": new ElementInfo("atom:logo", null, atomLogo, false),
+ "atom:entry": new ElementInfo("entries", Cc[ENTRY_CONTRACTID],
+ null, true),
+ },
+
+ "IN_ENTRIES": {
+ "atom:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ null, true),
+ "atom:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID],
+ null, true),
+ "atom:link": new ElementInfo("links", null, null, true),
+ },
+
+ /** ******* ATOM 0.3 **********/
+ "IN_ATOM03": {
+ "atom03:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ null, true),
+ "atom03:contributor": new ElementInfo("contributors",
+ Cc[PERSON_CONTRACTID],
+ null, true),
+ "atom03:link": new ElementInfo("links", null, null, true),
+ "atom03:entry": new ElementInfo("atom03_entries", Cc[ENTRY_CONTRACTID],
+ null, true),
+ "atom03:generator": new ElementInfo("generator", Cc[GENERATOR_CONTRACTID],
+ atomGenerator, false),
+ },
+
+ "IN_ATOM03_ENTRIES": {
+ "atom03:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ null, true),
+ "atom03:contributor": new ElementInfo("contributors",
+ Cc[PERSON_CONTRACTID],
+ null, true),
+ "atom03:link": new ElementInfo("links", null, null, true),
+ "atom03:entry": new ElementInfo("atom03_entries", Cc[ENTRY_CONTRACTID],
+ null, true),
+ },
+ };
+}
+
+// See startElement for a long description of how feeds are processed.
+FeedProcessor.prototype = {
+
+ // Set ourselves as the SAX handler, and set the base URI
+ _init: function FP_init(uri) {
+ this._reader.contentHandler = this;
+ this._reader.errorHandler = this;
+ this._result = Cc[FR_CONTRACTID].createInstance(Ci.nsIFeedResult);
+ if (uri) {
+ this._result.uri = uri;
+ this._reader.baseURI = uri;
+ this._xmlBaseStack[0] = uri;
+ }
+ },
+
+ // This function is called once we figure out what type of feed
+ // we're dealing with. Some feed types require digging a bit further
+ // than the root.
+ _docVerified: function FP_docVerified(version) {
+ this._result.doc = Cc[FEED_CONTRACTID].createInstance(Ci.nsIFeed);
+ this._result.doc.baseURI =
+ this._xmlBaseStack[this._xmlBaseStack.length - 1];
+ this._result.doc.fields = this._feed;
+ this._result.version = version;
+ },
+
+ // When we're done with the feed, let the listener know what
+ // happened.
+ _sendResult: function FP_sendResult() {
+ this._haveSentResult = true;
+ try {
+ // Can be null when a non-feed is fed to us
+ if (this._result.doc)
+ this._result.doc.normalize();
+ } catch (e) {
+ LOG("FIXME: " + e);
+ }
+
+ try {
+ if (this.listener != null)
+ this.listener.handleResult(this._result);
+ } finally {
+ this._result = null;
+ }
+ },
+
+ // Parsing functions
+ parseAsync: function FP_parseAsync(requestObserver, uri) {
+ this._init(uri);
+ this._reader.parseAsync(requestObserver);
+ },
+
+ // nsIStreamListener
+
+ // The XMLReader will throw sensible exceptions if these get called
+ // out of order.
+ onStartRequest: function FP_onStartRequest(request, context) {
+ // this will throw if the request is not a channel, but so will nsParser.
+ var channel = request.QueryInterface(Ci.nsIChannel);
+ channel.contentType = "application/vnd.mozilla.maybe.feed";
+ this._reader.onStartRequest(request, context);
+ },
+
+ onStopRequest: function FP_onStopRequest(request, context, statusCode) {
+ try {
+ this._reader.onStopRequest(request, context, statusCode);
+ } finally {
+ this._reader = null;
+ }
+ },
+
+ onDataAvailable:
+ function FP_onDataAvailable(request, context, inputStream, offset, count) {
+ this._reader.onDataAvailable(request, context, inputStream, offset, count);
+ },
+
+ // nsISAXErrorHandler
+
+ // We only care about fatal errors. When this happens, we may have
+ // parsed through the feed metadata and some number of entries. The
+ // listener can still show some of that data if it wants, and we'll
+ // set the bozo bit to indicate we were unable to parse all the way
+ // through.
+ fatalError: function FP_reportError() {
+ this._result.bozo = true;
+ // XXX need to QI to FeedProgressListener
+ if (!this._haveSentResult)
+ this._sendResult();
+ },
+
+ // nsISAXContentHandler
+
+ startDocument: function FP_startDocument() {
+ // LOG("----------");
+ },
+
+ endDocument: function FP_endDocument() {
+ if (!this._haveSentResult)
+ this._sendResult();
+ },
+
+ // The transitions defined above identify elements that contain more
+ // than just text. For example RSS items contain many fields, and so
+ // do Atom authors. The only commonly used elements that contain
+ // mixed content are Atom Text Constructs of type="xhtml", which we
+ // delegate to another handler for cleaning. That leaves a couple
+ // different types of elements to deal with: those that should occur
+ // only once, such as title elements, and those that can occur
+ // multiple times, such as the RSS category element and the Atom
+ // link element. Most of the RSS1/DC elements can occur multiple
+ // times in theory, but in practice, the only ones that do have
+ // analogues in Atom.
+ //
+ // Some elements are also groups of attributes or sub-elements,
+ // while others are simple text fields. For the most part, we don't
+ // have to pay explicit attention to the simple text elements,
+ // unless we want to post-process the resulting string to transform
+ // it into some richer object like a Date or URI.
+ //
+ // Elements that have more sophisticated content models still end up
+ // being dictionaries, whether they are based on attributes like RSS
+ // cloud, sub-elements like Atom author, or even items and
+ // entries. These elements are treated as "containers". It's
+ // theoretically possible for a container to have an attribute with
+ // the same universal name as a sub-element, but none of the feed
+ // formats allow this by default, and I don't of any extension that
+ // works this way.
+ //
+ startElement: function FP_startElement(uri, localName, qName, attributes) {
+ this._buf = "";
+ ++this._depth;
+ var elementInfo;
+
+ // LOG("<" + localName + ">");
+
+ // Check for xml:base
+ var base = attributes.getValueFromName(XMLNS, "base");
+ if (base) {
+ this._xmlBaseStack[this._depth] =
+ strToURI(base, this._xmlBaseStack[this._xmlBaseStack.length - 1]);
+ }
+
+ // To identify the element we're dealing with, we look up the
+ // namespace URI in our gNamespaces dictionary, which will give us
+ // a "canonical" prefix for a namespace URI. For example, this
+ // allows Dublin Core "creator" elements to be consistently mapped
+ // to "dc:creator", for easy field access by consumer code. This
+ // strategy also happens to shorten up our state table.
+ var key = this._prefixForNS(uri) + localName;
+
+ // Check to see if we need to hand this off to our XHTML handler.
+ // The elements we're dealing with will look like this:
+ //
+ //
+ //
+ // A title with bold and italics.
+ //
+ //
+ //
+ // When it returns in returnFromXHTMLHandler, the handler should
+ // give us back a string like this:
+ //
+ // "A title with bold and italics."
+ //
+ // The Atom spec explicitly says the div is not part of the content,
+ // and explicitly allows whitespace collapsing.
+ //
+ if ((this._result.version == "atom" || this._result.version == "atom03") &&
+ this._textConstructs[key] != null) {
+ var type = attributes.getValueFromName("", "type");
+ if (type != null && type.includes("xhtml")) {
+ this._xhtmlHandler =
+ new XHTMLHandler(this, (this._result.version == "atom"));
+ this._reader.contentHandler = this._xhtmlHandler;
+ return;
+ }
+ }
+
+ // Check our current state, and see if that state has a defined
+ // transition. For example, this._trans["atom:entry"]["atom:author"]
+ // will have one, and it tells us to add an item to our authors array.
+ if (this._trans[this._state] && this._trans[this._state][key]) {
+ elementInfo = this._trans[this._state][key];
+ } else {
+ // If we don't have a transition, hand off to extension handler
+ this._extensionHandler = new ExtensionHandler(this);
+ this._reader.contentHandler = this._extensionHandler;
+ this._extensionHandler.startElement(uri, localName, qName, attributes);
+ return;
+ }
+
+ // This distinguishes wrappers like 'channel' from elements
+ // we'd actually like to do something with (which will test true).
+ this._handlerStack[this._depth] = elementInfo;
+ if (elementInfo.isWrapper) {
+ this._state = "IN_" + elementInfo.fieldName.toUpperCase();
+ this._stack.push([this._feed, this._state]);
+ } else if (elementInfo.feedVersion) {
+ this._state = "IN_" + elementInfo.fieldName.toUpperCase();
+
+ // Check for the older RSS2 variants
+ if (elementInfo.feedVersion == "rss2")
+ elementInfo.feedVersion = this._findRSSVersion(attributes);
+ else if (uri == RSS090NS)
+ elementInfo.feedVersion = "rss090";
+
+ this._docVerified(elementInfo.feedVersion);
+ this._stack.push([this._feed, this._state]);
+ this._mapAttributes(this._feed, attributes);
+ } else {
+ this._state = this._processComplexElement(elementInfo, attributes);
+ }
+ },
+
+ // In the endElement handler, we decrement the stack and look
+ // for cleanup/transition functions to execute. The second part
+ // of the state transition works as above in startElement, but
+ // the state we're looking for is prefixed with an underscore
+ // to distinguish endElement events from startElement events.
+ endElement: function FP_endElement(uri, localName, qName) {
+ var elementInfo = this._handlerStack[this._depth];
+ // LOG("" + localName + ">");
+ if (elementInfo && !elementInfo.isWrapper)
+ this._closeComplexElement(elementInfo);
+
+ // cut down xml:base context
+ if (this._xmlBaseStack.length == this._depth + 1)
+ this._xmlBaseStack = this._xmlBaseStack.slice(0, this._depth);
+
+ // our new state is whatever is at the top of the stack now
+ if (this._stack.length > 0)
+ this._state = this._stack[this._stack.length - 1][1];
+ this._handlerStack = this._handlerStack.slice(0, this._depth);
+ --this._depth;
+ },
+
+ // Buffer up character data. The buffer is cleared with every
+ // opening element.
+ characters: function FP_characters(data) {
+ this._buf += data;
+ },
+
+ processingInstruction: function FP_processingInstruction(target, data) {
+ if (target == "xml-stylesheet") {
+ var hrefAttribute = data.match(/href=[\"\'](.*?)[\"\']/);
+ if (hrefAttribute && hrefAttribute.length == 2)
+ this._result.stylesheet = strToURI(hrefAttribute[1], this._result.uri);
+ }
+ },
+
+ // end of nsISAXContentHandler
+
+ // Handle our more complicated elements--those that contain
+ // attributes and child elements.
+ _processComplexElement:
+ function FP__processComplexElement(elementInfo, attributes) {
+ var obj;
+
+ // If the container is an entry/item, it'll need to have its
+ // more esoteric properties put in the 'fields' property bag.
+ if (elementInfo.containerClass == Cc[ENTRY_CONTRACTID]) {
+ obj = elementInfo.containerClass.createInstance(Ci.nsIFeedEntry);
+ obj.baseURI = this._xmlBaseStack[this._xmlBaseStack.length - 1];
+ this._mapAttributes(obj.fields, attributes);
+ } else if (elementInfo.containerClass) {
+ obj = elementInfo.containerClass.createInstance(Ci.nsIFeedElementBase);
+ obj.baseURI = this._xmlBaseStack[this._xmlBaseStack.length - 1];
+ obj.attributes = attributes; // just set the SAX attributes
+ } else {
+ obj = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2);
+ this._mapAttributes(obj, attributes);
+ }
+
+ // We should have a container/propertyBag that's had its
+ // attributes processed. Now we need to attach it to its
+ // container.
+ var newProp;
+
+ // First we'll see what's on top of the stack.
+ var container = this._stack[this._stack.length - 1][0];
+
+ // Check to see if it has the property
+ var prop;
+ try {
+ prop = container.getProperty(elementInfo.fieldName);
+ } catch (e) {
+ }
+
+ if (elementInfo.isArray) {
+ if (!prop) {
+ container.setPropertyAsInterface(elementInfo.fieldName,
+ Cc[ARRAY_CONTRACTID].
+ createInstance(Ci.nsIMutableArray));
+ }
+
+ newProp = container.getProperty(elementInfo.fieldName);
+ // XXX This QI should not be necessary, but XPConnect seems to fly
+ // off the handle in the browser, and loses track of the interface
+ // on large files. Bug 335638.
+ newProp.QueryInterface(Ci.nsIMutableArray);
+ newProp.appendElement(obj);
+
+ // If new object is an nsIFeedContainer, we want to deal with
+ // its member nsIPropertyBag instead.
+ if (isIFeedContainer(obj))
+ newProp = obj.fields;
+
+ } else {
+ // If it doesn't, set it.
+ if (!prop) {
+ container.setPropertyAsInterface(elementInfo.fieldName, obj);
+ }
+ newProp = container.getProperty(elementInfo.fieldName);
+ }
+
+ // make our new state name, and push the property onto the stack
+ var newState = "IN_" + elementInfo.fieldName.toUpperCase();
+ this._stack.push([newProp, newState, obj]);
+ return newState;
+ },
+
+ // Sometimes we need reconcile the element content with the object
+ // model for a given feed. We use helper functions to do the
+ // munging, but we need to identify array types here, so the munging
+ // happens only to the last element of an array.
+ _closeComplexElement: function FP__closeComplexElement(elementInfo) {
+ var stateTuple = this._stack.pop();
+ var container = stateTuple[0];
+ var containerParent = stateTuple[2];
+ var element = null;
+ var isArray = isIArray(container);
+
+ // If it's an array and we have to post-process,
+ // grab the last element
+ if (isArray)
+ element = container.queryElementAt(container.length - 1, Ci.nsISupports);
+ else
+ element = container;
+
+ // Run the post-processing function if there is one.
+ if (elementInfo.closeFunc)
+ element = elementInfo.closeFunc(this._buf, element);
+
+ // If an nsIFeedContainer was on top of the stack,
+ // we need to normalize it
+ if (elementInfo.containerClass == Cc[ENTRY_CONTRACTID])
+ containerParent.normalize();
+
+ // If it's an array, re-set the last element
+ if (isArray)
+ container.replaceElementAt(element, container.length - 1);
+ },
+
+ _prefixForNS: function FP_prefixForNS(uri) {
+ if (!uri)
+ return "";
+ var prefix = gNamespaces[uri];
+ if (prefix)
+ return prefix + ":";
+ if (uri.toLowerCase().indexOf("http://backend.userland.com") == 0)
+ return "";
+ return null;
+ },
+
+ _mapAttributes: function FP__mapAttributes(bag, attributes) {
+ // Cycle through the attributes, and set our properties using the
+ // prefix:localNames we find in our namespace dictionary.
+ for (var i = 0; i < attributes.length; ++i) {
+ var key = this._prefixForNS(attributes.getURI(i)) + attributes.getLocalName(i);
+ var val = attributes.getValue(i);
+ bag.setPropertyAsAString(key, val);
+ }
+ },
+
+ // Only for RSS2esque formats
+ _findRSSVersion: function FP__findRSSVersion(attributes) {
+ var versionAttr = attributes.getValueFromName("", "version").trim();
+ var versions = { "0.91": "rss091",
+ "0.92": "rss092",
+ "0.93": "rss093",
+ "0.94": "rss094" };
+ if (versions[versionAttr])
+ return versions[versionAttr];
+ if (versionAttr.substr(0, 2) != "2.")
+ return "rssUnknown";
+ return "rss2";
+ },
+
+ // unknown element values are returned here. See startElement above
+ // for how this works.
+ returnFromExtHandler:
+ function FP_returnExt(uri, localName, chars, attributes) {
+ --this._depth;
+
+ // take control of the SAX events
+ this._reader.contentHandler = this;
+ if (localName == null && chars == null)
+ return;
+
+ // we don't take random elements inside rdf:RDF
+ if (this._state == "IN_RDF")
+ return;
+
+ // Grab the top of the stack
+ var top = this._stack[this._stack.length - 1];
+ if (!top)
+ return;
+
+ var container = top[0];
+ // Grab the last element if it's an array
+ if (isIArray(container)) {
+ var contract = this._handlerStack[this._depth].containerClass;
+ // check if it's something specific, but not an entry
+ if (contract && contract != Cc[ENTRY_CONTRACTID]) {
+ var el = container.queryElementAt(container.length - 1,
+ Ci.nsIFeedElementBase);
+ // XXX there must be a way to flatten these interfaces
+ if (contract == Cc[PERSON_CONTRACTID])
+ el.QueryInterface(Ci.nsIFeedPerson);
+ else
+ return; // don't know about this interface
+
+ let propName = localName;
+ var prefix = gNamespaces[uri];
+
+ // synonyms
+ if ((uri == "" ||
+ prefix &&
+ ((prefix.indexOf("atom") > -1) ||
+ (prefix.indexOf("rss") > -1))) &&
+ (propName == "url" || propName == "href"))
+ propName = "uri";
+
+ try {
+ if (el[propName] !== "undefined") {
+ var propValue = chars;
+ // convert URI-bearing values to an nsIURI
+ if (propName == "uri") {
+ var base = this._xmlBaseStack[this._xmlBaseStack.length - 1];
+ propValue = strToURI(chars, base);
+ }
+ el[propName] = propValue;
+ }
+ } catch (e) {
+ // ignore XPConnect errors
+ }
+ // the rest of the function deals with entry- and feed-level stuff
+ return;
+ }
+ container = container.queryElementAt(container.length - 1,
+ Ci.nsIWritablePropertyBag2);
+ }
+
+ // Make the buffer our new property
+ var propName = this._prefixForNS(uri) + localName;
+
+ // But, it could be something containing HTML. If so,
+ // we need to know about that.
+ if (this._textConstructs[propName] != null &&
+ this._handlerStack[this._depth].containerClass !== null) {
+ var newProp = Cc[TEXTCONSTRUCT_CONTRACTID].
+ createInstance(Ci.nsIFeedTextConstruct);
+ newProp.text = chars;
+ // Look up the default type in our table
+ var type = this._textConstructs[propName];
+ var typeAttribute = attributes.getValueFromName("", "type");
+ if (this._result.version == "atom" && typeAttribute != null) {
+ type = typeAttribute;
+ } else if (this._result.version == "atom03" && typeAttribute != null) {
+ if (typeAttribute.toLowerCase().includes("xhtml")) {
+ type = "xhtml";
+ } else if (typeAttribute.toLowerCase().includes("html")) {
+ type = "html";
+ } else if (typeAttribute.toLowerCase().includes("text")) {
+ type = "text";
+ }
+ }
+
+ // If it's rss feed-level description, it's not supposed to have html
+ if (this._result.version.includes("rss") &&
+ this._handlerStack[this._depth].containerClass != ENTRY_CONTRACTID) {
+ type = "text";
+ }
+ newProp.type = type;
+ newProp.base = this._xmlBaseStack[this._xmlBaseStack.length - 1];
+ container.setPropertyAsInterface(propName, newProp);
+ } else {
+ container.setPropertyAsAString(propName, chars);
+ }
+ },
+
+ // Sometimes, we'll hand off SAX handling duties to an XHTMLHandler
+ // (see above) that will scrape out non-XHTML stuff, normalize
+ // namespaces, and remove the wrapper div from Atom 1.0. When the
+ // XHTMLHandler is done, it'll callback here.
+ returnFromXHTMLHandler:
+ function FP_returnFromXHTMLHandler(chars, uri, localName, qName) {
+ // retake control of the SAX content events
+ this._reader.contentHandler = this;
+
+ // Grab the top of the stack
+ var top = this._stack[this._stack.length - 1];
+ if (!top)
+ return;
+ var container = top[0];
+
+ // Assign the property
+ var newProp = newProp = Cc[TEXTCONSTRUCT_CONTRACTID].
+ createInstance(Ci.nsIFeedTextConstruct);
+ newProp.text = chars;
+ newProp.type = "xhtml";
+ newProp.base = this._xmlBaseStack[this._xmlBaseStack.length - 1];
+ container.setPropertyAsInterface(this._prefixForNS(uri) + localName,
+ newProp);
+
+ // XHTML will cause us to peek too far. The XHTML handler will
+ // send us an end element to call. RFC4287-valid feeds allow a
+ // more graceful way to handle this. Unfortunately, we can't count
+ // on compliance at this point.
+ this.endElement(uri, localName, qName);
+ },
+
+ // XPCOM stuff
+ classID: FP_CLASSID,
+ QueryInterface: ChromeUtils.generateQI(
+ [Ci.nsIFeedProcessor, Ci.nsISAXContentHandler, Ci.nsISAXErrorHandler,
+ Ci.nsIStreamListener, Ci.nsIRequestObserver]
+ ),
+};
+
+var components = [FeedProcessor, FeedResult, Feed, Entry,
+ TextConstruct, Generator, Person];
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/resource/feeds/nsIFeed.idl b/resource/feeds/nsIFeed.idl
new file mode 100644
index 0000000000..ad87ad9d3e
--- /dev/null
+++ b/resource/feeds/nsIFeed.idl
@@ -0,0 +1,86 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIFeedContainer.idl"
+
+interface nsIArray;
+interface nsIFeedGenerator;
+
+/**
+ * An nsIFeed represents a single Atom or RSS feed.
+ */
+[scriptable, uuid(3b8aae33-80e2-4efa-99c8-a6c5b99f76ea)]
+interface nsIFeed : nsIFeedContainer
+{
+ /**
+ * Uses description, subtitle, and extensions
+ * to generate a summary.
+ */
+ attribute nsIFeedTextConstruct subtitle;
+
+ // All content classifies as a "feed" - it is the transport.
+ const unsigned long TYPE_FEED = 0;
+ const unsigned long TYPE_AUDIO = 1;
+ const unsigned long TYPE_IMAGE = 2;
+ const unsigned long TYPE_VIDEO = 4;
+
+ /**
+ * The type of feed. For example, a podcast would be TYPE_AUDIO.
+ */
+ readonly attribute unsigned long type;
+
+ /**
+ * The total number of enclosures found in the feed.
+ */
+ attribute long enclosureCount;
+
+ /**
+ * The items or entries in feed.
+ */
+ attribute nsIArray items;
+
+ /**
+ * No one really knows what cloud is for.
+ *
+ * It supposedly enables some sort of interaction with an XML-RPC or
+ * SOAP service.
+ */
+ attribute nsIWritablePropertyBag2 cloud;
+
+ /**
+ * Information about the software that produced the feed.
+ */
+ attribute nsIFeedGenerator generator;
+
+ /**
+ * An image url and some metadata (as defined by RSS2).
+ *
+ */
+ attribute nsIWritablePropertyBag2 image;
+
+ /**
+ * No one really knows what textInput is for.
+ *
+ * See
+ *
+ * for more details.
+ */
+ attribute nsIWritablePropertyBag2 textInput;
+
+ /**
+ * Days to skip fetching. This field was supposed to designate
+ * intervals for feed fetching. It's not generally implemented. For
+ * example, if this array contained "Monday", aggregators should not
+ * fetch the feed on Mondays.
+ */
+ attribute nsIArray skipDays;
+
+ /**
+ * Hours to skip fetching. This field was supposed to designate
+ * intervals for feed fetching. It's not generally implemented. See
+ * for more information.
+ */
+ attribute nsIArray skipHours;
+};
diff --git a/resource/feeds/nsIFeedContainer.idl b/resource/feeds/nsIFeedContainer.idl
new file mode 100644
index 0000000000..58de494a51
--- /dev/null
+++ b/resource/feeds/nsIFeedContainer.idl
@@ -0,0 +1,85 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIFeedElementBase.idl"
+
+interface nsIURI;
+interface nsIWritablePropertyBag2;
+interface nsIArray;
+interface nsIFeedTextConstruct;
+
+/**
+ * A shared base for feeds and items, which are pretty similar,
+ * but they have some divergent attributes and require
+ * different convenience methods.
+ */
+[scriptable, uuid(577a1b4c-b3d4-4c76-9cf8-753e6606114f)]
+interface nsIFeedContainer : nsIFeedElementBase
+{
+ /**
+ * Many feeds contain an ID distinct from their URI, and
+ * entries have standard fields for this in all major formats.
+ */
+ attribute AString id;
+
+ /**
+ * The fields found in the document. Common Atom
+ * and RSS fields are normalized. This includes some namespaced
+ * extensions such as dc:subject and content:encoded.
+ * Consumers can avoid normalization by checking the feed type
+ * and accessing specific fields.
+ *
+ * Common namespaces are accessed using prefixes, like get("dc:subject");.
+ * See nsIFeedResult::registerExtensionPrefix.
+ */
+ attribute nsIWritablePropertyBag2 fields;
+
+ /**
+ * Sometimes there's no title, or the title contains markup, so take
+ * care in decoding the attribute.
+ */
+ attribute nsIFeedTextConstruct title;
+
+ /**
+ * Returns the primary link for the feed or entry.
+ */
+ attribute nsIURI link;
+
+ /**
+ * Returns all links for a feed or entry.
+ */
+ attribute nsIArray links;
+
+ /**
+ * Returns the categories found in a feed or entry.
+ */
+ attribute nsIArray categories;
+
+ /**
+ * The rights or license associated with a feed or entry.
+ */
+ attribute nsIFeedTextConstruct rights;
+
+ /**
+ * A list of nsIFeedPersons that authored the feed.
+ */
+ attribute nsIArray authors;
+
+ /**
+ * A list of nsIFeedPersons that contributed to the feed.
+ */
+ attribute nsIArray contributors;
+
+ /**
+ * The date the feed was updated, in RFC822 form. Parsable by JS
+ * and mail code.
+ */
+ attribute AString updated;
+
+ /**
+ * Syncs a container's fields with its convenience attributes.
+ */
+ void normalize();
+};
diff --git a/resource/feeds/nsIFeedElementBase.idl b/resource/feeds/nsIFeedElementBase.idl
new file mode 100644
index 0000000000..1b8975ae5a
--- /dev/null
+++ b/resource/feeds/nsIFeedElementBase.idl
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsISAXAttributes;
+interface nsIURI;
+
+/**
+ * An nsIFeedGenerator represents the software used to create a feed.
+ */
+[scriptable, uuid(5215291e-fa0a-40c2-8ce7-e86cd1a1d3fa)]
+interface nsIFeedElementBase : nsISupports
+{
+ /**
+ * The attributes found on the element. Most interfaces provide convenience
+ * accessors for their standard fields, so this useful only when looking for
+ * an extension.
+ */
+ attribute nsISAXAttributes attributes;
+
+ /**
+ * The baseURI for the Entry or Feed.
+ */
+ attribute nsIURI baseURI;
+};
diff --git a/resource/feeds/nsIFeedEntry.idl b/resource/feeds/nsIFeedEntry.idl
new file mode 100644
index 0000000000..83646aadb6
--- /dev/null
+++ b/resource/feeds/nsIFeedEntry.idl
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIFeedContainer.idl"
+interface nsIArray;
+
+/**
+ * An nsIFeedEntry represents an Atom or RSS entry/item. Summary
+ * and/or full-text content may be available, but callers will have to
+ * check both.
+ */
+[scriptable, uuid(31bfd5b4-8ff5-4bfd-a8cb-b3dfbd4f0a5b)]
+interface nsIFeedEntry : nsIFeedContainer {
+
+ /**
+ * Uses description, subtitle, summary, content and extensions
+ * to generate a summary.
+ *
+ */
+ attribute nsIFeedTextConstruct summary;
+
+ /**
+ * The date the entry was published, in RFC822 form. Parsable by JS
+ * and mail code.
+ */
+ attribute AString published;
+
+ /**
+ * Uses atom:content and content:encoded to provide
+ * a 'full text' view of an entry.
+ *
+ */
+ attribute nsIFeedTextConstruct content;
+
+ /**
+ * Enclosures are podcasts, photocasts, etc.
+ */
+ attribute nsIArray enclosures;
+
+ /**
+ * Enclosures, etc. that might be displayed inline.
+ */
+ attribute nsIArray mediaContent;
+};
diff --git a/resource/feeds/nsIFeedGenerator.idl b/resource/feeds/nsIFeedGenerator.idl
new file mode 100644
index 0000000000..3c23ca1424
--- /dev/null
+++ b/resource/feeds/nsIFeedGenerator.idl
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIFeedElementBase.idl"
+
+interface nsIURI;
+
+/**
+ * An nsIFeedGenerator represents the software used to create a feed.
+ */
+[scriptable, uuid(0fecd56b-bd92-481b-a486-b8d489cdd385)]
+interface nsIFeedGenerator : nsIFeedElementBase
+{
+ /**
+ * The name of the software.
+ */
+ attribute AString agent;
+
+ /**
+ * The version of the software.
+ */
+ attribute AString version;
+
+ /**
+ * A URI associated with the software.
+ */
+ attribute nsIURI uri;
+};
diff --git a/resource/feeds/nsIFeedListener.idl b/resource/feeds/nsIFeedListener.idl
new file mode 100644
index 0000000000..6826d04a41
--- /dev/null
+++ b/resource/feeds/nsIFeedListener.idl
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+interface nsIFeedResult;
+interface nsIFeedEntry;
+
+/**
+ * nsIFeedResultListener defines a callback used when feed processing
+ * completes.
+ */
+[scriptable, uuid(4d2ebe88-36eb-4e20-bcd1-997b3c1f24ce)]
+interface nsIFeedResultListener : nsISupports
+{
+ /**
+ * Always called, even after an error. There could be new feed-level
+ * data available at this point, if it followed or was interspersed
+ * with the items. Fire-and-Forget implementations only need this.
+ *
+ * @param result
+ * An object implementing nsIFeedResult representing the feed
+ * and its metadata.
+ */
+ void handleResult(in nsIFeedResult result);
+};
+
+
+/**
+ * nsIFeedProgressListener defines callbacks used during feed
+ * processing.
+ */
+[scriptable, uuid(ebfd5de5-713c-40c0-ad7c-f095117fa580)]
+interface nsIFeedProgressListener : nsIFeedResultListener {
+
+ /**
+ * ReportError will be called in the event of fatal
+ * XML errors, or if the document is not a feed. The bozo
+ * bit will be set if the error was due to a fatal error.
+ *
+ * @param errorText
+ * A short description of the error.
+ * @param lineNumber
+ * The line on which the error occurred.
+ */
+ void reportError(in AString errorText, in long lineNumber,
+ in boolean bozo);
+
+ /**
+ * StartFeed will be called as soon as a reasonable start to
+ * a feed is detected.
+ *
+ * @param result
+ * An object implementing nsIFeedResult representing the feed
+ * and its metadata. At this point, the result has version
+ * information.
+ */
+ void handleStartFeed(in nsIFeedResult result);
+
+ /**
+ * Called when the first entry/item is encountered. In Atom, all
+ * feed data is required to preceed the entries. In RSS, the data
+ * usually does. If the type is one of the entry/item-only types,
+ * this event will not be called.
+ *
+ * @param result
+ * An object implementing nsIFeedResult representing the feed
+ * and its metadata. At this point, the result will likely have
+ * most of its feed-level metadata.
+ */
+ void handleFeedAtFirstEntry(in nsIFeedResult result);
+
+ /**
+ * Called after each entry/item. If the document is a standalone
+ * item or entry, this HandleFeedAtFirstEntry will not have been
+ * called. Also, this entry's parent field will be null.
+ *
+ * @param entry
+ * An object implementing nsIFeedEntry that represents the latest
+ * entry encountered.
+ * @param result
+ * An object implementing nsIFeedResult representing the feed
+ * and its metadata.
+ */
+ void handleEntry(in nsIFeedEntry entry, in nsIFeedResult result);
+};
diff --git a/resource/feeds/nsIFeedPerson.idl b/resource/feeds/nsIFeedPerson.idl
new file mode 100644
index 0000000000..d9d6eb77bf
--- /dev/null
+++ b/resource/feeds/nsIFeedPerson.idl
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIFeedElementBase.idl"
+
+interface nsIURI;
+
+/**
+ * An nsIFeedPerson represents an author or contributor of a feed.
+ */
+[scriptable, uuid(29cbd45f-f2d3-4b28-b557-3ab7a61ecde4)]
+interface nsIFeedPerson : nsIFeedElementBase
+{
+ /**
+ * The name of the person.
+ */
+ attribute AString name;
+
+ /**
+ * An email address associated with the person.
+ */
+ attribute AString email;
+
+ /**
+ * A URI associated with the person (e.g. a homepage).
+ */
+ attribute nsIURI uri;
+};
diff --git a/resource/feeds/nsIFeedProcessor.idl b/resource/feeds/nsIFeedProcessor.idl
new file mode 100644
index 0000000000..eb695e4e77
--- /dev/null
+++ b/resource/feeds/nsIFeedProcessor.idl
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIStreamListener.idl"
+
+interface nsIURI;
+interface nsIFeedResultListener;
+interface nsIInputStream;
+
+/**
+ * An nsIFeedProcessor parses feeds, triggering callbacks based on
+ * their contents.
+ */
+[scriptable, uuid(8a0b2908-21b0-45d7-b14d-30df0f92afc7)]
+interface nsIFeedProcessor : nsIStreamListener {
+
+ /**
+ * The listener that will respond to feed events.
+ */
+ attribute nsIFeedResultListener listener;
+
+ // Level is where to listen for the extension, a constant: FEED,
+ // ENTRY, BOTH.
+ //
+ // XXX todo void registerExtensionHandler(in
+ // nsIFeedExtensionHandler, in long level);
+
+ /**
+ * Parse a feed asynchronously. The caller must then call the
+ * nsIFeedProcessor's nsIStreamListener methods to drive the
+ * parse. Do not call the other parse methods during an asynchronous
+ * parse.
+ *
+ * @param requestObserver The observer to notify on start/stop. This
+ * argument can be null.
+ * @param uri The base URI.
+ */
+ void parseAsync(in nsIRequestObserver requestObserver, in nsIURI uri);
+};
diff --git a/resource/feeds/nsIFeedResult.idl b/resource/feeds/nsIFeedResult.idl
new file mode 100644
index 0000000000..4cfb0a13ea
--- /dev/null
+++ b/resource/feeds/nsIFeedResult.idl
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+interface nsIFeedContainer;
+interface nsIProperties;
+interface nsIURI;
+
+/**
+ * The nsIFeedResult interface provides access to HTTP and parsing
+ * metadata for a feed or entry.
+ */
+[scriptable, uuid(7a180b78-0f46-4569-8c22-f3d720ea1c57)]
+interface nsIFeedResult : nsISupports {
+
+ /**
+ * The Feed parser will set the bozo bit when a feed triggers a fatal
+ * error during XML parsing. There may be entries and feed metadata
+ * that were parsed before the error. Thanks to Tim Bray for
+ * suggesting this terminology.
+ *
+ */
+ attribute boolean bozo;
+
+ /**
+ * The parsed feed or entry.
+ *
+ * Will be null if a non-feed is processed.
+ */
+ attribute nsIFeedContainer doc;
+
+ /**
+ * The address from which the feed was fetched.
+ */
+ attribute nsIURI uri;
+
+ /**
+ * Feed Version:
+ * atom, rss2, rss09, rss091, rss091userland, rss092, rss1, atom03,
+ * atomEntry, rssItem
+ *
+ * Will be null if a non-feed is processed.
+ */
+ attribute AString version;
+
+ /**
+ * An XSLT stylesheet available to transform the source of the
+ * feed. Some feeds include this information in a processing
+ * instruction. It's generally intended for clients with specific
+ * feed capabilities.
+ */
+ attribute nsIURI stylesheet;
+
+ /**
+ * HTTP response headers that accompanied the feed.
+ */
+ attribute nsIProperties headers;
+
+ /**
+ * Registers a prefix used to access an extension in the feed/entry
+ */
+ void registerExtensionPrefix(in AString aNamespace, in AString aPrefix);
+};
diff --git a/resource/feeds/nsIFeedTextConstruct.idl b/resource/feeds/nsIFeedTextConstruct.idl
new file mode 100644
index 0000000000..9c77e9c0ff
--- /dev/null
+++ b/resource/feeds/nsIFeedTextConstruct.idl
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+
+webidl DocumentFragment;
+webidl Element;
+
+/**
+ * nsIFeedTextConstructs represent feed text fields that can contain
+ * one of text, HTML, or XHTML. Some extension elements also have "type"
+ * parameters, and this interface could be used there as well.
+ */
+[scriptable, uuid(fc97a2a9-d649-4494-931e-db81a156c873)]
+interface nsIFeedTextConstruct : nsISupports
+{
+ /**
+ * If the text construct contains (X)HTML, relative references in
+ * the content should be resolved against this base URI.
+ */
+ attribute nsIURI base;
+
+ /**
+ * The language of the text. For example, "en-US" for US English.
+ */
+ attribute AString lang;
+
+ /**
+ * One of "text", "html", or "xhtml". If the type is (x)html, a '<'
+ * character represents markup. To display that character, an escape
+ * such as < must be used. If the type is "text", the '<'
+ * character represents the character itself, and such text should
+ * not be embedded in markup without escaping it first.
+ */
+ attribute AString type;
+
+ /**
+ * The content of the text construct.
+ */
+ attribute AString text;
+
+ /**
+ * Returns the text of the text construct, with all markup stripped
+ * and all entities decoded. If the type attribute's value is "text",
+ * this function returns the value of the text attribute unchanged.
+ */
+ AString plainText();
+
+ /**
+ * Return an nsIDocumentFragment containing the text and markup.
+ */
+ DocumentFragment createDocumentFragment(in Element element);
+};
+