From 48a6e90b4f01186e5fd758fcfc377a71b0332601 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 3 May 2021 23:29:18 +0100 Subject: [PATCH 01/21] Initial import of feed processor This imports Mozilla's feed processor which has been removed upstream as part of Firefox 66. Some changes are likely needed before this will actually run in Zotero. --- resource/feeds/FeedProcessor.js | 1735 +++++++++++++++++++++++ resource/feeds/nsIFeed.idl | 86 ++ resource/feeds/nsIFeedContainer.idl | 85 ++ resource/feeds/nsIFeedElementBase.idl | 28 + resource/feeds/nsIFeedEntry.idl | 46 + resource/feeds/nsIFeedGenerator.idl | 30 + resource/feeds/nsIFeedListener.idl | 87 ++ resource/feeds/nsIFeedPerson.idl | 30 + resource/feeds/nsIFeedProcessor.idl | 41 + resource/feeds/nsIFeedResult.idl | 65 + resource/feeds/nsIFeedTextConstruct.idl | 58 + 11 files changed, 2291 insertions(+) create mode 100644 resource/feeds/FeedProcessor.js create mode 100644 resource/feeds/nsIFeed.idl create mode 100644 resource/feeds/nsIFeedContainer.idl create mode 100644 resource/feeds/nsIFeedElementBase.idl create mode 100644 resource/feeds/nsIFeedEntry.idl create mode 100644 resource/feeds/nsIFeedGenerator.idl create mode 100644 resource/feeds/nsIFeedListener.idl create mode 100644 resource/feeds/nsIFeedPerson.idl create mode 100644 resource/feeds/nsIFeedProcessor.idl create mode 100644 resource/feeds/nsIFeedResult.idl create mode 100644 resource/feeds/nsIFeedTextConstruct.idl 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 += ""; + } + }, + 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: + // + // + // <div xmlns="http://www.w3.org/1999/xhtml"> + // A title with <b>bold</b> and <i>italics</i>. + // </div> + // + // + // 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(""); + 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); +}; + From 71dc4e9bf9286d02e527b66ab3ea62aae203856c Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 7 May 2021 14:07:40 +0100 Subject: [PATCH 02/21] Convert feed processor indentation to Zotero style This adjust feed processor indentation to match Zotero conventions for tabs and continuous indents in blocks. --- resource/feeds/FeedProcessor.js | 2603 +++++++++++------------ resource/feeds/nsIFeed.idl | 119 +- resource/feeds/nsIFeedContainer.idl | 109 +- resource/feeds/nsIFeedElementBase.idl | 21 +- resource/feeds/nsIFeedEntry.idl | 51 +- resource/feeds/nsIFeedGenerator.idl | 25 +- resource/feeds/nsIFeedListener.idl | 117 +- resource/feeds/nsIFeedPerson.idl | 25 +- resource/feeds/nsIFeedProcessor.idl | 43 +- resource/feeds/nsIFeedResult.idl | 87 +- resource/feeds/nsIFeedTextConstruct.idl | 63 +- 11 files changed, 1626 insertions(+), 1637 deletions(-) diff --git a/resource/feeds/FeedProcessor.js b/resource/feeds/FeedProcessor.js index 6daf8d70af..7d64618bbf 100644 --- a/resource/feeds/FeedProcessor.js +++ b/resource/feeds/FeedProcessor.js @@ -1,10 +1,9 @@ -/* -*- 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"); + dump("*** " + str + "\n"); } ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); @@ -24,11 +23,11 @@ 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}"); + 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}"); + 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}"); @@ -47,46 +46,46 @@ 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; - } + base = base || null; + try { + return Services.io.newURI(link, null, base); + } catch (e) { + return null; + } } function isArray(a) { - return isObject(a) && a.constructor == Array; + return isObject(a) && a.constructor == Array; } function isObject(a) { - return (a && typeof a == "object") || isFunction(a); + return (a && typeof a == "object") || isFunction(a); } function isFunction(a) { - return typeof a == "function"; + return typeof a == "function"; } function isIID(a, iid) { - var rv = false; - try { - a.QueryInterface(iid); - rv = true; - } catch (e) { - } - return rv; + var rv = false; + try { + a.QueryInterface(iid); + rv = true; + } catch (e) { + } + return rv; } function isIArray(a) { - return isIID(a, Ci.nsIArray); + return isIID(a, Ci.nsIArray); } function isIFeedContainer(a) { - return isIID(a, Ci.nsIFeedContainer); + return isIID(a, Ci.nsIFeedContainer); } function stripTags(someHTML) { - return someHTML.replace(/<[^>]+>/g, ""); + return someHTML.replace(/<[^>]+>/g, ""); } /** @@ -95,604 +94,604 @@ function stripTags(someHTML) { */ 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; + 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(//g, ">"); + s = s.replace(/ 0) { - ++entries_with_enclosures; + if (entry.enclosures && entry.enclosures.length > 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"); + 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; - } - } - } - } + 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; + 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; + // 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 (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; - } - } + } 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; - }, + 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); - } - }, + _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); - }, + _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); - } + _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; - }, + 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); - } - } - } - }, + // 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]), + // 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); + 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, + 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"], - }, + 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); + normalize: function Entry_normalize() { + fieldsToObj(this, this.searchLists); - // Assign Atom link if needed - if (bagHasKey(this.fields, "links")) - this._atomLinksToURI(); + // Assign Atom link if needed + if (bagHasKey(this.fields, "links")) + this._atomLinksToURI(); - // Populate enclosures array - this._populateEnclosures(); + // 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; + // 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 (bagHasKey(guid, "isPermaLink")) + isPermaLink = guid.getProperty("isPermaLink").toLowerCase() != "false"; - if (guid && isPermaLink) - this.link = strToURI(guid.getProperty("guid")); - } + 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); + 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]); - }, + this._resetBagMembersToRawText([this.searchLists.content, + this.searchLists.summary, + this.searchLists.title]); + }, - _populateEnclosures: function Entry_populateEnclosures() { - if (bagHasKey(this.fields, "links")) - this._atomLinksToEnclosures(); + _populateEnclosures: function Entry_populateEnclosures() { + if (bagHasKey(this.fields, "links")) + this._atomLinksToEnclosures(); - // Add RSS2 enclosure to enclosures - if (bagHasKey(this.fields, "enclosure")) - this._enclosureToEnclosures(); + // 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: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: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"); - }, + // Add media:content in media:group to enclosures + if (bagHasKey(this.fields, "mediagroup")) + this._mediaToEnclosures("mediagroup", "mediacontent"); + }, - __enclosure_map: null, + __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; + _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 = {}; + if (this.__enclosure_map == null) + this.__enclosure_map = {}; - var previous_enc = this.__enclosure_map[new_enc.getPropertyAsAString("url")]; + var previous_enc = this.__enclosure_map[new_enc.getPropertyAsAString("url")]; - if (previous_enc != undefined) { - previous_enc.QueryInterface(Ci.nsIWritablePropertyBag2); + 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, "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")); + if (!bagHasKey(previous_enc, "length") && bagHasKey(new_enc, "length")) + previous_enc.setPropertyAsAString("length", new_enc.getPropertyAsAString("length")); - return; - } + return; + } - if (this.enclosures == null) { - this.enclosures = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray); - this.enclosures.QueryInterface(Ci.nsIMutableArray); - } + 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; - }, + 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; + _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]; + 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; + // an enclosure must have an href + if (!(link.getProperty("href"))) + return; - var enc = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2); + 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")); + // 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); - } - }, + this._addToEnclosures(enc); + } + }, - _enclosureToEnclosures: function Entry_enclosureToEnclosures() { - var enc = this.fields.getPropertyAsInterface("enclosure", Ci.nsIPropertyBag2); + _enclosureToEnclosures: function Entry_enclosureToEnclosures() { + var enc = this.fields.getPropertyAsInterface("enclosure", Ci.nsIPropertyBag2); - if (!(enc.getProperty("url"))) - return; + if (!(enc.getProperty("url"))) + return; - this._addToEnclosures(enc); - }, + this._addToEnclosures(enc); + }, - _mediaToEnclosures: function Entry_mediaToEnclosures(mediaType, contentType) { - var content; + _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); - } + // 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); + 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; + // 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); + 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); - } + // 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")); - } + if (bagHasKey(contentElement, "fileSize")) { + enc.setPropertyAsAString("length", contentElement.getPropertyAsAString("fileSize")); + } - this._addToEnclosures(enc); - } - }, + this._addToEnclosures(enc); + } + }, - // XPCOM stuff - classID: ENTRY_CLASSID, - QueryInterface: ChromeUtils.generateQI( - [Ci.nsIFeedEntry, Ci.nsIFeedContainer] - ), + // 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; + 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); + 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; - }, + 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; + 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); - }, + 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]), + // 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; + this.lang = null; + this.agent = null; + this.version = null; + this.uri = null; - // nsIFeedElementBase - this._attributes = null; - this.baseURI = null; + // nsIFeedElementBase + this._attributes = null; + this.baseURI = null; } Generator.prototype = { - get attributes() { - return this._attributes; - }, + 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); + 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); - } - }, + // 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] - ), + // XPCOM stuff + classID: GENERATOR_CLASSID, + QueryInterface: ChromeUtils.generateQI( + [Ci.nsIFeedGenerator, Ci.nsIFeedElementBase] + ), }; function Person() { - this.name = null; - this.uri = null; - this.email = null; + this.name = null; + this.uri = null; + this.email = null; - // nsIFeedElementBase - this.attributes = null; - this.baseURI = null; + // nsIFeedElementBase + this.attributes = null; + this.baseURI = null; } Person.prototype = { - // XPCOM stuff - classID: PERSON_CLASSID, - QueryInterface: ChromeUtils.generateQI( - [Ci.nsIFeedPerson, Ci.nsIFeedElementBase] - ), + // XPCOM stuff + classID: PERSON_CLASSID, + QueryInterface: ChromeUtils.generateQI( + [Ci.nsIFeedPerson, Ci.nsIFeedElementBase] + ), }; /** @@ -704,23 +703,23 @@ Person.prototype = { * 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; - } - } - } + 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; + } + } + } } /** @@ -730,33 +729,33 @@ function fieldsToObj(container, fields) { * @returns The lower case localName property of the specified element */ function LC(element) { - return element.localName.toLowerCase(); + 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; + 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()); + 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; + // 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; + guid.setPropertyAsAString("guid", s.trim()); + return guid; } // post-process an RSS author element @@ -773,33 +772,33 @@ function rssGuid(s, guid) { // 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; + 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; } // @@ -807,11 +806,11 @@ function rssAuthor(s, author) { // 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; + var str = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + str.data = s; + str.QueryInterface(Ci.nsISupportsString); + return str; } /** @@ -821,117 +820,117 @@ function rssArrayElement(s) { * @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; + 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 = []; + 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; - }, + // 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([]); + 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; + // 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)); + // 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 + "'"); + // 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(); + // 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; + // 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 += ""; - } - }, - characters: function XH_characters(data) { - this._buf += xmlEscape(data); - }, - processingInstruction: function XH_processingInstruction() { - }, + // 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 += ""; + } + }, + characters: function XH_characters(data) { + this._buf += xmlEscape(data); + }, + processingInstruction: function XH_processingInstruction() { + }, }; /** @@ -939,53 +938,53 @@ XHTMLHandler.prototype = { * added to our transition table in the FeedProcessor. */ function ExtensionHandler(processor) { - this._buf = ""; - this._depth = 0; - this._hasChildElements = false; + this._buf = ""; + this._depth = 0; + this._hasChildElements = false; - // The FeedProcessor - this._processor = processor; + // The FeedProcessor + this._processor = processor; - // Fields of the outermost extension element. - this._localName = null; - this._uri = null; - this._qName = null; - this._attrs = null; + // 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; + 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 (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); + // 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() { - }, + }, + 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() { + }, }; @@ -996,20 +995,20 @@ ExtensionHandler.prototype = { * 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; + 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; + this.isWrapper = false; + this.fieldName = fieldName; + this.feedVersion = feedVersion; } /** @@ -1018,718 +1017,718 @@ function FeedElementInfo(fieldName, feedVersion) { * of during parsing. */ function WrapperElementInfo(fieldName) { - this.isWrapper = true; - this.fieldName = 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; + 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; + // 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 = []; + // 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"), + 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 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"), + // 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"), - }, + // Treat as Atom 0.3 + "atom03:feed": new FeedElementInfo("Atom03", "atom03"), + }, - /** ******* RSS2 **********/ - "IN_RSS2": { - "channel": new WrapperElementInfo("channel"), - }, + /** ******* 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_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_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_SKIPDAYS": { + "day": new ElementInfo("days", null, rssArrayElement, true), + }, - "IN_SKIPHOURS": { - "hour": new ElementInfo("hours", 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), - }, + "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), - }, + /** ******* 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), - }, + "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), - }, + /** ******* 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), - }, + "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), - }, + /** ******* 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), - }, - }; + "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; - } - }, + // 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; - }, + // 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); - } + // 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; - } - }, + 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); - }, + // Parsing functions + parseAsync: function FP_parseAsync(requestObserver, uri) { + this._init(uri); + this._reader.parseAsync(requestObserver); + }, - // nsIStreamListener + // 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); - }, + // 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; - } - }, + 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); - }, + onDataAvailable: + function FP_onDataAvailable(request, context, inputStream, offset, count) { + this._reader.onDataAvailable(request, context, inputStream, offset, count); + }, - // nsISAXErrorHandler + // 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(); - }, + // 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 + // nsISAXContentHandler - startDocument: function FP_startDocument() { - // LOG("----------"); - }, + startDocument: function FP_startDocument() { + // LOG("----------"); + }, - endDocument: function FP_endDocument() { - if (!this._haveSentResult) - this._sendResult(); - }, + 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; + // 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 + ">"); + // 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]); - } + // 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; + // 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: - // - // - // <div xmlns="http://www.w3.org/1999/xhtml"> - // A title with <b>bold</b> and <i>italics</i>. - // </div> - // - // - // 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 to see if we need to hand this off to our XHTML handler. + // The elements we're dealing with will look like this: + // + // + // <div xmlns="http://www.w3.org/1999/xhtml"> + // A title with <b>bold</b> and <i>italics</i>. + // </div> + // + // + // 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; - } + // 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(); + // 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"; + // 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); - } - }, + 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(""); - if (elementInfo && !elementInfo.isWrapper) - this._closeComplexElement(elementInfo); + // 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(""); + 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); + // 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; - }, + // 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; - }, + // 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); - } - }, + 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 + // end of nsISAXContentHandler - // Handle our more complicated elements--those that contain - // attributes and child elements. - _processComplexElement: - function FP__processComplexElement(elementInfo, attributes) { - var obj; + // 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); - } + // 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; + // 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]; + // 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) { - } + // 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)); - } + 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); + 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; + // 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); - } + } 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; - }, + // 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); + // 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; + // 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); + // 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 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); - }, + // 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; - }, + _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); - } - }, + _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"; - }, + // 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; + // 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; + // 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; + // 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; + // 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 + 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]; + 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"; + // 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); - } + 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; + // 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"; - } - } + // 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); - } - }, + // 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; + // 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]; + // 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); + // 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); - }, + // 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] - ), + // 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]; + TextConstruct, Generator, Person]; this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); diff --git a/resource/feeds/nsIFeed.idl b/resource/feeds/nsIFeed.idl index ad87ad9d3e..c1156016a9 100644 --- a/resource/feeds/nsIFeed.idl +++ b/resource/feeds/nsIFeed.idl @@ -1,4 +1,3 @@ -/* -*- 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/. */ @@ -14,73 +13,73 @@ interface nsIFeedGenerator; [scriptable, uuid(3b8aae33-80e2-4efa-99c8-a6c5b99f76ea)] interface nsIFeed : nsIFeedContainer { - /** - * Uses description, subtitle, and extensions - * to generate a summary. - */ - attribute nsIFeedTextConstruct subtitle; + /** + * 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; + // 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 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; + /** + * 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; + /** + * 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; + /** + * Information about the software that produced the feed. + */ + attribute nsIFeedGenerator generator; - /** - * An image url and some metadata (as defined by RSS2). - * - */ - attribute nsIWritablePropertyBag2 image; + /** + * 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; + /** + * 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; + /** + * 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; + * 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 index 58de494a51..d288035eef 100644 --- a/resource/feeds/nsIFeedContainer.idl +++ b/resource/feeds/nsIFeedContainer.idl @@ -1,4 +1,3 @@ -/* -*- 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/. */ @@ -18,68 +17,68 @@ interface nsIFeedTextConstruct; [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; + /** + * 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; + /** + * 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; + /** + * 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 the primary link for the feed or entry. + */ + attribute nsIURI link; - /** - * Returns all links for a feed or entry. - */ - attribute nsIArray links; + /** + * Returns all links for a feed or entry. + */ + attribute nsIArray links; - /** - * Returns the categories found in a feed or entry. - */ - attribute nsIArray categories; + /** + * 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; + /** + * 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 authored the feed. + */ + attribute nsIArray authors; - /** - * A list of nsIFeedPersons that contributed to the feed. - */ - attribute nsIArray contributors; + /** + * 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; + /** + * 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(); + /** + * Syncs a container's fields with its convenience attributes. + */ + void normalize(); }; diff --git a/resource/feeds/nsIFeedElementBase.idl b/resource/feeds/nsIFeedElementBase.idl index 1b8975ae5a..a8364160ca 100644 --- a/resource/feeds/nsIFeedElementBase.idl +++ b/resource/feeds/nsIFeedElementBase.idl @@ -1,4 +1,3 @@ -/* -*- 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/. */ @@ -14,15 +13,15 @@ interface nsIURI; [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 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; + /** + * The baseURI for the Entry or Feed. + */ + attribute nsIURI baseURI; }; diff --git a/resource/feeds/nsIFeedEntry.idl b/resource/feeds/nsIFeedEntry.idl index 83646aadb6..88778e9966 100644 --- a/resource/feeds/nsIFeedEntry.idl +++ b/resource/feeds/nsIFeedEntry.idl @@ -1,4 +1,3 @@ -/* -*- 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/. */ @@ -14,33 +13,33 @@ interface nsIArray; [scriptable, uuid(31bfd5b4-8ff5-4bfd-a8cb-b3dfbd4f0a5b)] interface nsIFeedEntry : nsIFeedContainer { - /** - * Uses description, subtitle, summary, content and extensions - * to generate a summary. - * - */ - attribute nsIFeedTextConstruct summary; + /** + * 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; + /** + * 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; + /** + * 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 are podcasts, photocasts, etc. + */ + attribute nsIArray enclosures; - /** - * Enclosures, etc. that might be displayed inline. - */ - attribute nsIArray mediaContent; + /** + * Enclosures, etc. that might be displayed inline. + */ + attribute nsIArray mediaContent; }; diff --git a/resource/feeds/nsIFeedGenerator.idl b/resource/feeds/nsIFeedGenerator.idl index 3c23ca1424..4ec956543f 100644 --- a/resource/feeds/nsIFeedGenerator.idl +++ b/resource/feeds/nsIFeedGenerator.idl @@ -1,4 +1,3 @@ -/* -*- 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/. */ @@ -13,18 +12,18 @@ interface nsIURI; [scriptable, uuid(0fecd56b-bd92-481b-a486-b8d489cdd385)] interface nsIFeedGenerator : nsIFeedElementBase { - /** - * The name of the software. - */ - attribute AString agent; + /** + * The name of the software. + */ + attribute AString agent; - /** - * The version of the software. - */ - attribute AString version; + /** + * The version of the software. + */ + attribute AString version; - /** - * A URI associated with the software. - */ - attribute nsIURI uri; + /** + * A URI associated with the software. + */ + attribute nsIURI uri; }; diff --git a/resource/feeds/nsIFeedListener.idl b/resource/feeds/nsIFeedListener.idl index 6826d04a41..8bca4b716a 100644 --- a/resource/feeds/nsIFeedListener.idl +++ b/resource/feeds/nsIFeedListener.idl @@ -1,4 +1,3 @@ -/* -*- 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/. */ @@ -14,16 +13,16 @@ interface nsIFeedEntry; [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); + /** + * 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); }; @@ -34,54 +33,54 @@ interface nsIFeedResultListener : nsISupports [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); + /** + * 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 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); + /** + * 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 index d9d6eb77bf..77de267f60 100644 --- a/resource/feeds/nsIFeedPerson.idl +++ b/resource/feeds/nsIFeedPerson.idl @@ -1,4 +1,3 @@ -/* -*- 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/. */ @@ -13,18 +12,18 @@ interface nsIURI; [scriptable, uuid(29cbd45f-f2d3-4b28-b557-3ab7a61ecde4)] interface nsIFeedPerson : nsIFeedElementBase { - /** - * The name of the person. - */ - attribute AString name; + /** + * The name of the person. + */ + attribute AString name; - /** - * An email address associated with the person. - */ - attribute AString email; + /** + * An email address associated with the person. + */ + attribute AString email; - /** - * A URI associated with the person (e.g. a homepage). - */ - attribute nsIURI uri; + /** + * 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 index eb695e4e77..483dfe656b 100644 --- a/resource/feeds/nsIFeedProcessor.idl +++ b/resource/feeds/nsIFeedProcessor.idl @@ -1,4 +1,3 @@ -/* -*- 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/. */ @@ -16,26 +15,26 @@ interface nsIInputStream; [scriptable, uuid(8a0b2908-21b0-45d7-b14d-30df0f92afc7)] interface nsIFeedProcessor : nsIStreamListener { - /** - * The listener that will respond to feed events. - */ - attribute nsIFeedResultListener listener; + /** + * 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); + // 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 index 4cfb0a13ea..beb8420fe0 100644 --- a/resource/feeds/nsIFeedResult.idl +++ b/resource/feeds/nsIFeedResult.idl @@ -1,4 +1,3 @@ -/* -*- 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/. */ @@ -14,52 +13,52 @@ interface nsIURI; */ [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 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; + /** + * 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; + /** + * 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; + /** + * 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; + /** + * 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); + /** + * 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 index 9c77e9c0ff..893611b05b 100644 --- a/resource/feeds/nsIFeedTextConstruct.idl +++ b/resource/feeds/nsIFeedTextConstruct.idl @@ -1,4 +1,3 @@ -/* -*- 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/. */ @@ -18,41 +17,41 @@ webidl Element; [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; + /** + * 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; + /** + * 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; + /** + * 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; + /** + * 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(); + /** + * 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); + /** + * Return an nsIDocumentFragment containing the text and markup. + */ + DocumentFragment createDocumentFragment(in Element element); }; From bb9b796efe4f49640c4c3842190ba658464c8898 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 7 May 2021 14:39:21 +0100 Subject: [PATCH 03/21] Tweak test runner to support more directories This expands the test filtering in the test runner so that you can supply e.g. `test/tests/feed*` from your shell in the `zotero` directory and correctly filter to all feed-related tests. --- test/content/runtests.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/content/runtests.js b/test/content/runtests.js index 3ba7bfa6f3..714537a6f1 100644 --- a/test/content/runtests.js +++ b/test/content/runtests.js @@ -210,12 +210,13 @@ var assert = chai.assert, // Set up tests to run var run = ZoteroUnit.runTests; -if(run && ZoteroUnit.tests) { +if (run && ZoteroUnit.tests) { function getTestFilename(test) { - // Allow foo, fooTest, fooTest.js, and tests/fooTest.js + // Remove any directory prefixes e.g. tests/fooTest.js, test/tests/fooTest.js + test = test.split(/[/\\]/).pop(); + // Allow foo, fooTest, fooTest.js test = test.replace(/\.js$/, ""); test = test.replace(/Test$/, ""); - test = test.replace(/^tests[/\\]/, ""); return test + "Test.js"; } @@ -284,4 +285,4 @@ if(run) { return mocha.run(); }) }; -} \ No newline at end of file +} From bc4aafa8e42e2e9ea190f0b0fc36777167d8d1a9 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Sun, 16 May 2021 17:47:02 +0100 Subject: [PATCH 04/21] Add feed reader tests for parsing behavior This adds extra tests to check parsing behavior such as entities, tag handling, CDATA, etc. This will help ensure the new feed processor matches the previous behavior. --- test/tests/data/feedCDATA.rss | 68 ++++++++++++++++++++++++++++++++ test/tests/data/feedMedia.xml | 31 +++++++++++++++ test/tests/data/feedRichText.rss | 30 ++++++++++++++ test/tests/feedReaderTest.js | 62 +++++++++++++++++++++++------ 4 files changed, 179 insertions(+), 12 deletions(-) create mode 100644 test/tests/data/feedCDATA.rss create mode 100644 test/tests/data/feedMedia.xml create mode 100644 test/tests/data/feedRichText.rss diff --git a/test/tests/data/feedCDATA.rss b/test/tests/data/feedCDATA.rss new file mode 100644 index 0000000000..8809b806ab --- /dev/null +++ b/test/tests/data/feedCDATA.rss @@ -0,0 +1,68 @@ + + + + + Science current issue + http://science.sciencemag.org + Science RSS feed -- current issue + 1095-9203 + May 21 2021 12:00:00:000AM + Science + 0036-8075 + + + + + + + + + + Science + http://science.sciencemag.org/icons/banner/title.gif + http://science.sciencemag.org + + + <![CDATA["The Descent of Man," 150 years on]]> + http://science.sciencemag.org/cgi/content/short/372/6544/769?rss=1 + + + 2021-05-20T10:40:55-07:00 + info:doi/10.1126/science.abj4606 + hwp:resource-id:sci;372/6544/769 + American Association for the Advancement of Science + + + 2021-05-21 + editorial + 372 + 6544 + 769 + 769 + + + <![CDATA[News at a glance]]> + http://science.sciencemag.org/cgi/content/short/372/6544/770?rss=1 + + + 2021-05-20T10:40:55-07:00 + info:doi/10.1126/science.372.6544.770 + hwp:resource-id:sci;372/6544/770 + American Association for the Advancement of Science + + + 2021-05-21 + In Brief + 372 + 6544 + 770 + 772 + + diff --git a/test/tests/data/feedMedia.xml b/test/tests/data/feedMedia.xml new file mode 100644 index 0000000000..e9452ad740 --- /dev/null +++ b/test/tests/data/feedMedia.xml @@ -0,0 +1,31 @@ + + + + + NYT > Top Stories + https://www.nytimes.com + + + en-us + Copyright 2021 The New York Times Company + Wed, 16 Jun 2021 19:30:15 +0000 + Wed, 16 Jun 2021 19:20:47 +0000 + + In Pictures: President Biden’s Trip to Europe + https://www.nytimes.com/2021/06/10/world/europe/biden-europe-pictures.html + https://www.nytimes.com/2021/06/10/world/europe/biden-europe-pictures.html + + The president is in Cornwall, England, to meet with other leaders of wealthy democracies. + Wed, 16 Jun 2021 18:53:17 +0000 + Biden, Joseph R Jr + Johnson, Boris + Biden, Jill Tracy Jacobs + Group of Seven + North Atlantic Treaty Organization + Coronavirus (2019-nCoV) + Europe + + Doug Mills/The New York Times + + + diff --git a/test/tests/data/feedRichText.rss b/test/tests/data/feedRichText.rss new file mode 100644 index 0000000000..dd1073b595 --- /dev/null +++ b/test/tests/data/feedRichText.rss @@ -0,0 +1,30 @@ + + + + + Liftoff News + http://liftoff.msfc.nasa.gov/ + Liftoff to Space Exploration. + en-us + Tue, 10 Jun 2003 04:00:00 GMT + Tue, 10 Jun 2003 09:41:01 GMT + http://blogs.law.harvard.edu/tech/rss + Weblog Editor 2.0 + editor@example.com + webmaster@example.com + + Encoded "entity" + http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp + They take a crash course in language & protocol. + Tue, 03 Jun 2003 09:39:21 GMT + http://liftoff.msfc.nasa.gov/2003/06/03.html#item573 + + + Embedded <b>tags</b> + http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp + The proposed <b>VASIMR</b> engine would do that. + Tue, 27 May 2003 08:37:32 GMT + http://liftoff.msfc.nasa.gov/2003/05/27.html#item571 + + + diff --git a/test/tests/feedReaderTest.js b/test/tests/feedReaderTest.js index 6eaceab270..7b34a3efc2 100644 --- a/test/tests/feedReaderTest.js +++ b/test/tests/feedReaderTest.js @@ -30,18 +30,10 @@ describe("Zotero.FeedReader", function () { language: 'en' }; + var richTextRSSFeedURL = getTestDataUrl("feedRichText.rss"); + var cdataRSSFeedURL = getTestDataUrl("feedCDATA.rss"); var atomFeedURL = getTestDataUrl("feed.atom"); - var atomFeedInfo = { - title: 'Incircular nets and confocal conics', - updated: new Date("Tue, 10 Jun 2003 09:41:01 GMT"), - creators: [{ - firstName: '', - lastName: 'editor@example.com', - creatorType: 'author', - fieldMode: 1 - }], - language: 'en-us' - }; + var mediaFeedURL = getTestDataUrl("feedMedia.xml"); after(function* () { yield clearFeeds(); @@ -200,5 +192,51 @@ describe("Zotero.FeedReader", function () { while(item = yield itemIterator.next().value); assert.isNull(item); }); + + it('should decode entities', async () => { + const fr = new Zotero.FeedReader(richTextRSSFeedURL); + await fr.process(); + const itemIterator = new fr.ItemIterator(); + const item = await itemIterator.next().value; + + assert.equal(item.title, `Encoded "entity"`); + assert.equal(item.abstractNote, "They take a crash course in language & protocol."); + }); + + it('should remove tags', async () => { + const fr = new Zotero.FeedReader(richTextRSSFeedURL); + await fr.process(); + const itemIterator = new fr.ItemIterator(); + let item; + for (let i = 0; i < 2; i++) { + // eslint-disable-next-line no-await-in-loop + item = await itemIterator.next().value; + } + + // The entry title is text only, so tags are just more text. + assert.equal(item.title, "Embedded tags"); + // The entry description is XHTML, so tags are removed there. + assert.equal(item.abstractNote, "The proposed VASIMR engine would do that."); + }); + + it('should parse CDATA as text', async () => { + const fr = new Zotero.FeedReader(cdataRSSFeedURL); + await fr.process(); + const itemIterator = new fr.ItemIterator(); + const item = await itemIterator.next().value; + + assert.equal(item.title, `"The Descent of Man," 150 years on`); + assert.equal(item.creators[0].lastName, "Fuentes"); + }); + + it('should parse enclosed media', async () => { + const fr = new Zotero.FeedReader(mediaFeedURL); + await fr.process(); + const itemIterator = new fr.ItemIterator(); + const item = await itemIterator.next().value; + + assert.equal(item.enclosedItems.length, 1); + assert.equal(item.enclosedItems[0].url, "https://static01.nyt.com/images/2021/06/16/world/16biden-photos1/16biden-photos1-moth.jpg"); + }); }); -}) \ No newline at end of file +}) From 088483b77687e27f20014a3122babc8850502e65 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 7 May 2021 15:19:58 +0100 Subject: [PATCH 05/21] Fix lint issues in feedReader.js --- chrome/content/zotero/xpcom/feedReader.js | 152 +++++++++++----------- 1 file changed, 77 insertions(+), 75 deletions(-) diff --git a/chrome/content/zotero/xpcom/feedReader.js b/chrome/content/zotero/xpcom/feedReader.js index 66022bab14..239120015a 100644 --- a/chrome/content/zotero/xpcom/feedReader.js +++ b/chrome/content/zotero/xpcom/feedReader.js @@ -53,16 +53,15 @@ * @method {void} terminate Stops retrieving/parsing the feed. Data parsed up * to this point is still available. */ -Zotero.FeedReader = function(url) { +Zotero.FeedReader = function (url) { if (!url) throw new Error("Feed URL must be supplied"); - this._url = url; this._feedItems = [Zotero.Promise.defer()]; this._feedProcessed = Zotero.Promise.defer(); - + let feedFetched = Zotero.Promise.defer(); - feedFetched.promise.then(function(feed) { + feedFetched.promise.then(function (feed) { let info = {}; info.title = feed.title ? feed.title.plainText() : ''; @@ -93,7 +92,7 @@ Zotero.FeedReader = function(url) { if (issn) info.ISSN = issn; let isbn = Zotero.FeedReader._getFeedField(feed, 'isbn', 'prism') - || Zotero.FeedReader._getFeedField(feed, 'isbn') + || Zotero.FeedReader._getFeedField(feed, 'isbn'); if (isbn) info.ISBN = isbn; let language = Zotero.FeedReader._getFeedField(feed, 'language', 'dc') @@ -105,10 +104,10 @@ Zotero.FeedReader = function(url) { this._feedProperties = info; this._feed = feed; - }.bind(this)).then(function(){ + }.bind(this)).then(function () { let items = this._feed.items; if (items && items.length) { - for (let i=0; i { if (!result.doc) { this.terminate("No Feed"); @@ -156,12 +156,12 @@ Zotero.FeedReader = function(url) { Zotero.debug("FeedReader: Fetching feed from " + feedUrl.spec); - this._channel = Services.io.newChannelFromURI2(feedUrl, null, - Services.scriptSecurityManager.getSystemPrincipal(), null, + this._channel = Services.io.newChannelFromURI2(feedUrl, null, + Services.scriptSecurityManager.getSystemPrincipal(), null, Ci.nsILoadInfo.SEC_NORMAL, Ci.nsIContentPolicy.TYPE_OTHER); this._channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE; this._channel.asyncOpen(feedProcessor, null); // Sends an HTTP request -} +}; /* * The constructor initiates async feed processing, but _feedProcessed @@ -175,7 +175,7 @@ Zotero.FeedReader.prototype.process = Zotero.Promise.coroutine(function* () { * Terminate feed processing at any given time * @param {String} status Reason for terminating processing */ -Zotero.FeedReader.prototype.terminate = function(status) { +Zotero.FeedReader.prototype.terminate = function (status) { Zotero.debug("FeedReader: Terminating feed reader (" + status + ")"); // Reject feed promise if not resolved yet @@ -203,11 +203,11 @@ Zotero.FeedReader.prototype.terminate = function(status) { }; Zotero.defineProperty(Zotero.FeedReader.prototype, 'feedProperties', { - get: function(){ + get: function () { if (!this._feedProperties) { - throw new Error("Feed has not been resolved yet. Try calling FeedReader#process first") + throw new Error("Feed has not been resolved yet. Try calling FeedReader#process first"); } - return this._feedProperties + return this._feedProperties; } }); @@ -220,18 +220,19 @@ Zotero.defineProperty(Zotero.FeedReader.prototype, 'feedProperties', { * for termination. */ Zotero.defineProperty(Zotero.FeedReader.prototype, 'ItemIterator', { - get: function() { + get: function () { let items = this._feedItems; + // eslint-disable-next-line consistent-this let feedReader = this; - let iterator = function() { + let iterator = function () { if (!feedReader._feedProperties) { - throw new Error("Feed has not been resolved yet. Try calling FeedReader#process first") + throw new Error("Feed has not been resolved yet. Try calling FeedReader#process first"); } this.index = 0; }; - iterator.prototype.next = function() { + iterator.prototype.next = function () { let item = items[this.index++]; return { value: item ? item.promise : null, @@ -239,23 +240,23 @@ Zotero.defineProperty(Zotero.FeedReader.prototype, 'ItemIterator', { }; }; - iterator.prototype.last = function() { - return items[items.length-1]; - } + iterator.prototype.last = function () { + return items[items.length - 1]; + }; return iterator; } -}, {lazy: true}); +}, { lazy: true }); /***************************** * Item processing functions * *****************************/ - + /** * Determine item type based on item data */ -Zotero.FeedReader._guessItemType = function(item) { +Zotero.FeedReader._guessItemType = function (item) { // Default to journalArticle item.itemType = 'journalArticle'; @@ -288,12 +289,12 @@ Zotero.FeedReader._guessItemType = function(item) { /* * Fetch creators from given field of a feed entry */ -Zotero.FeedReader._processCreators = function(feedEntry, field, role) { +Zotero.FeedReader._processCreators = function (feedEntry, field, role) { let names = [], nameStr; try { let personArr = feedEntry[field]; // Seems like this part can throw if there is no author data in the feed - for (let i=0; i 1 - // If only one comma and first part has more than one space, - // it's probably not lastName, firstName + && (other || commas > 1 + // If only one comma and first part has more than one space, + // it's probably not lastName, firstName || (commas == 1 && name.split(/\s*,/)[0].indexOf(' ') != -1) ) ) { // Probably multiple authors listed in a single field nameStr = name; break; // For clarity. personArr.length == 1 anyway - } else { + } + else { names.push(name); } } - } - catch(e) { + } + catch (e) { if (e.result != Components.results.NS_ERROR_FAILURE) throw e; if (field != 'authors') return []; @@ -335,7 +336,7 @@ Zotero.FeedReader._processCreators = function(feedEntry, field, role) { } let creators = []; - for (let i=0; i Date: Fri, 7 May 2021 17:45:01 +0100 Subject: [PATCH 06/21] More style alignment for feed processor --- resource/feeds/FeedProcessor.js | 516 ++++++++++++++++---------------- 1 file changed, 261 insertions(+), 255 deletions(-) diff --git a/resource/feeds/FeedProcessor.js b/resource/feeds/FeedProcessor.js index 7d64618bbf..1c5436260d 100644 --- a/resource/feeds/FeedProcessor.js +++ b/resource/feeds/FeedProcessor.js @@ -2,6 +2,8 @@ * 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/. */ +/* eslint-disable quote-props */ + function LOG(str) { dump("*** " + str + "\n"); } @@ -191,11 +193,11 @@ FeedResult.prototype = { headers: null, uri: null, stylesheet: null, - + registerExtensionPrefix: function FR_registerExtensionPrefix(ns, prefix) { throw Cr.NS_ERROR_NOT_IMPLEMENTED; }, - + // XPCOM stuff classID: FR_CLASSID, QueryInterface: ChromeUtils.generateQI([Ci.nsIFeedResult]), @@ -218,8 +220,13 @@ function Feed() { Feed.prototype = { searchLists: { title: ["title", "rss1:title", "atom03:title", "atom:title"], - subtitle: ["description", "dc:description", "rss1:description", - "atom03:tagline", "atom:subtitle"], + subtitle: [ + "description", + "dc:description", + "rss1:description", + "atom03:tagline", + "atom:subtitle", + ], items: ["items", "atom03_entries", "entries"], id: ["atom:id", "rdf:about"], generator: ["generator"], @@ -233,53 +240,59 @@ Feed.prototype = { textInput: ["textInput", "rss1:textinput"], skipDays: ["skipDays"], skipHours: ["skipHours"], - updated: ["pubDate", "lastBuildDate", "atom03:modified", "dc:date", - "dcterms:modified", "atom:updated"], + updated: [ + "pubDate", + "lastBuildDate", + "atom03:modified", + "dc:date", + "dcterms:modified", + "atom:updated", + ], }, - + normalize: function Feed_normalize() { fieldsToObj(this, this.searchLists); if (this.skipDays) this.skipDays = this.skipDays.getProperty("days"); if (this.skipHours) this.skipHours = this.skipHours.getProperty("hours"); - + if (this.updated) this.updated = dateParse(this.updated); - + // Assign Atom link if needed if (bagHasKey(this.fields, "links")) this._atomLinksToURI(); - + this._calcEnclosureCountAndFeedType(); - + // Resolve relative image links if (this.image && bagHasKey(this.image, "url")) this._resolveImageLink(); - + this._resetBagMembersToRawText([this.searchLists.subtitle, this.searchLists.title]); }, - + _calcEnclosureCountAndFeedType: function Feed_calcEnclosureCountAndFeedType() { var entries_with_enclosures = 0; var audio_count = 0; var image_count = 0; var video_count = 0; var other_count = 0; - + for (var i = 0; i < this.items.length; ++i) { var entry = this.items.queryElementAt(i, Ci.nsIFeedEntry); entry.QueryInterface(Ci.nsIFeedContainer); - + if (entry.enclosures && entry.enclosures.length > 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)) { @@ -295,9 +308,9 @@ Feed.prototype = { } } } - + var feedtype = Ci.nsIFeed.TYPE_FEED; - + // For a feed to be marked as TYPE_VIDEO, TYPE_AUDIO and TYPE_IMAGE, // we enforce two things: // @@ -309,19 +322,17 @@ Feed.prototype = { 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); @@ -333,7 +344,7 @@ Feed.prototype = { this.link = this._resolveURI(href, base); } }, - + _resolveImageLink: function Feed_resolveImageLink() { var base; if (bagHasKey(this.image, "xml:base")) @@ -342,7 +353,7 @@ Feed.prototype = { if (url) this.image.setPropertyAsAString("url", url.spec); }, - + _resolveURI: function Feed_resolveURI(linkSpec, baseSpec) { var uri = null; try { @@ -354,7 +365,7 @@ Feed.prototype = { return uri; }, - + // reset the bag to raw contents, not text constructs _resetBagMembersToRawText: function Feed_resetBagMembers(fieldLists) { for (var i = 0; i < fieldLists.length; i++) { @@ -367,7 +378,7 @@ Feed.prototype = { } } }, - + // XPCOM stuff classID: FEED_CLASSID, QueryInterface: ChromeUtils.generateQI([Ci.nsIFeed, Ci.nsIFeedContainer]), @@ -392,92 +403,106 @@ 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"], + id: [ + ["guid", makePropGetter("guid")], + "rdf:about", + "atom03:id", + "atom:id", + ], authors: ["authors"], contributors: ["contributors"], - summary: ["description", "rss1:description", "dc:description", - "atom03:summary", "atom:summary"], + 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"], + 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 { @@ -487,48 +512,48 @@ Entry.prototype = { } } 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); @@ -537,10 +562,10 @@ Entry.prototype = { 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) { @@ -549,17 +574,17 @@ Entry.prototype = { } 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")) { @@ -569,15 +594,15 @@ Entry.prototype = { 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( @@ -588,7 +613,7 @@ Entry.prototype = { Entry.prototype._atomLinksToURI = Feed.prototype._atomLinksToURI; Entry.prototype._resolveURI = Feed.prototype._resolveURI; Entry.prototype._resetBagMembersToRawText = - Feed.prototype._resetBagMembersToRawText; + Feed.prototype._resetBagMembersToRawText; // TextConstruct represents and element that could contain (X)HTML function TextConstruct() { @@ -609,7 +634,7 @@ TextConstruct.prototype = { } return this.text; }, - + createDocumentFragment: function TC_createDocumentFragment(element) { if (this.type == "text") { var doc = element.ownerDocument; @@ -625,12 +650,12 @@ TextConstruct.prototype = { 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]), @@ -642,25 +667,25 @@ function Generator() { 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"); + 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) { @@ -668,7 +693,7 @@ Generator.prototype = { this.uri = strToURI(uriAttribute, this.baseURI); } }, - + // XPCOM stuff classID: GENERATOR_CLASSID, QueryInterface: ChromeUtils.generateQI( @@ -680,7 +705,7 @@ function Person() { this.name = null; this.uri = null; this.email = null; - + // nsIFeedElementBase this.attributes = null; this.baseURI = null; @@ -846,7 +871,7 @@ function XHTMLHandler(processor, isAtom) { // 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) { @@ -858,7 +883,7 @@ XHTMLHandler.prototype = { } return false; }, - + startDocument: function XH_startDocument() { }, endDocument: function XH_endDocument() { @@ -866,13 +891,13 @@ XHTMLHandler.prototype = { 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; @@ -889,13 +914,13 @@ XHTMLHandler.prototype = { 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); @@ -910,11 +935,11 @@ XHTMLHandler.prototype = { 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(), @@ -941,10 +966,10 @@ 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; @@ -959,17 +984,16 @@ ExtensionHandler.prototype = { }, 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; @@ -1034,79 +1058,72 @@ function FeedProcessor() { 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._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), + "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), + "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), + "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), @@ -1114,20 +1131,20 @@ function FeedProcessor() { "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 @@ -1136,71 +1153,51 @@ function FeedProcessor() { "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), + "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: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), + "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: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: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), + "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: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: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; @@ -1212,7 +1209,7 @@ FeedProcessor.prototype = { 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. @@ -1223,7 +1220,7 @@ FeedProcessor.prototype = { 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() { @@ -1235,7 +1232,7 @@ FeedProcessor.prototype = { } catch (e) { LOG("FIXME: " + e); } - + try { if (this.listener != null) this.listener.handleResult(this._result); @@ -1243,15 +1240,15 @@ FeedProcessor.prototype = { 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) { @@ -1260,7 +1257,7 @@ FeedProcessor.prototype = { 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); @@ -1268,14 +1265,14 @@ FeedProcessor.prototype = { 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 @@ -1287,18 +1284,18 @@ FeedProcessor.prototype = { 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 @@ -1330,16 +1327,16 @@ FeedProcessor.prototype = { 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 @@ -1347,7 +1344,7 @@ FeedProcessor.prototype = { // 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: // @@ -1375,7 +1372,7 @@ FeedProcessor.prototype = { 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. @@ -1388,7 +1385,7 @@ FeedProcessor.prototype = { 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; @@ -1397,13 +1394,13 @@ FeedProcessor.prototype = { 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); @@ -1411,7 +1408,7 @@ FeedProcessor.prototype = { 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 @@ -1422,24 +1419,24 @@ FeedProcessor.prototype = { // LOG(""); 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=[\"\'](.*?)[\"\']/); @@ -1447,15 +1444,15 @@ FeedProcessor.prototype = { 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]) { @@ -1470,41 +1467,40 @@ FeedProcessor.prototype = { 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) { @@ -1512,13 +1508,13 @@ FeedProcessor.prototype = { } 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 @@ -1529,28 +1525,28 @@ FeedProcessor.prototype = { 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 ""; @@ -1561,7 +1557,7 @@ FeedProcessor.prototype = { 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. @@ -1571,7 +1567,7 @@ FeedProcessor.prototype = { bag.setPropertyAsAString(key, val); } }, - + // Only for RSS2esque formats _findRSSVersion: function FP__findRSSVersion(attributes) { var versionAttr = attributes.getValueFromName("", "version").trim(); @@ -1585,27 +1581,27 @@ FeedProcessor.prototype = { 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)) { @@ -1619,10 +1615,10 @@ FeedProcessor.prototype = { el.QueryInterface(Ci.nsIFeedPerson); else return; // don't know about this interface - + let propName = localName; var prefix = gNamespaces[uri]; - + // synonyms if ((uri == "" || prefix && @@ -1630,7 +1626,7 @@ FeedProcessor.prototype = { (prefix.indexOf("rss") > -1))) && (propName == "url" || propName == "href")) propName = "uri"; - + try { if (el[propName] !== "undefined") { var propValue = chars; @@ -1650,10 +1646,10 @@ FeedProcessor.prototype = { 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 && @@ -1675,7 +1671,7 @@ FeedProcessor.prototype = { 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) { @@ -1688,7 +1684,7 @@ FeedProcessor.prototype = { 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 @@ -1697,13 +1693,13 @@ FeedProcessor.prototype = { 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); @@ -1712,23 +1708,33 @@ FeedProcessor.prototype = { 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] - ), + QueryInterface: ChromeUtils.generateQI([ + Ci.nsIFeedProcessor, + Ci.nsISAXContentHandler, + Ci.nsISAXErrorHandler, + Ci.nsIStreamListener, + Ci.nsIRequestObserver, + ]), }; -var components = [FeedProcessor, FeedResult, Feed, Entry, - TextConstruct, Generator, Person]; +var components = [ + FeedProcessor, + FeedResult, + Feed, + Entry, + TextConstruct, + Generator, + Person, +]; this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); From 76873bda9554e20b7651b58365effca117d03f63 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Sat, 8 May 2021 17:15:38 +0100 Subject: [PATCH 07/21] Approaching readable code in feed processor This adjusts bracing style and other aesthetics to match Zotero conventions. --- resource/feeds/FeedProcessor.js | 533 +++++++++++++++++++------------- 1 file changed, 312 insertions(+), 221 deletions(-) diff --git a/resource/feeds/FeedProcessor.js b/resource/feeds/FeedProcessor.js index 1c5436260d..67654cbde3 100644 --- a/resource/feeds/FeedProcessor.js +++ b/resource/feeds/FeedProcessor.js @@ -24,12 +24,12 @@ 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_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_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}"); @@ -51,7 +51,8 @@ function strToURI(link, base) { base = base || null; try { return Services.io.newURI(link, null, base); - } catch (e) { + } + catch (e) { return null; } } @@ -73,7 +74,8 @@ function isIID(a, iid) { try { a.QueryInterface(iid); rv = true; - } catch (e) { + } + catch (e) { } return rv; } @@ -102,8 +104,9 @@ function findAtomLinks(rel, links) { // atom:link MUST have @href if (bagHasKey(linkElement, "href")) { var relAttribute = null; - if (bagHasKey(linkElement, "rel")) + if (bagHasKey(linkElement, "rel")) { relAttribute = linkElement.getPropertyAsAString("rel"); + } if ((!relAttribute && rel == "alternate") || relAttribute == rel) { rvLinks.push(linkElement); continue; @@ -140,16 +143,18 @@ function bagHasKey(bag, key) { try { bag.getProperty(key); return true; - } catch (e) { + } + catch (e) { return false; } } function makePropGetter(key) { - return function FeedPropGetter(bag) { + return function (bag) { try { return bag.getProperty(key); - } catch (e) { + } + catch (e) { } return null; }; @@ -194,7 +199,7 @@ FeedResult.prototype = { uri: null, stylesheet: null, - registerExtensionPrefix: function FR_registerExtensionPrefix(ns, prefix) { + registerExtensionPrefix: function (ns, prefix) { throw Cr.NS_ERROR_NOT_IMPLEMENTED; }, @@ -232,7 +237,7 @@ Feed.prototype = { generator: ["generator"], authors: ["authors"], contributors: ["contributors"], - link: [["link", strToURI], ["rss1:link", strToURI]], + link: [["link", strToURI], ["rss1:link", strToURI]], categories: ["categories", "dc:subject"], rights: ["atom03:rights", "atom:rights"], cloud: ["cloud"], @@ -250,31 +255,35 @@ Feed.prototype = { ], }, - normalize: function Feed_normalize() { + normalize: function () { fieldsToObj(this, this.searchLists); - if (this.skipDays) + if (this.skipDays) { this.skipDays = this.skipDays.getProperty("days"); - if (this.skipHours) + } + if (this.skipHours) { this.skipHours = this.skipHours.getProperty("hours"); + } - if (this.updated) + if (this.updated) { this.updated = dateParse(this.updated); + } // Assign Atom link if needed - if (bagHasKey(this.fields, "links")) + if (bagHasKey(this.fields, "links")) { this._atomLinksToURI(); + } this._calcEnclosureCountAndFeedType(); // Resolve relative image links - if (this.image && bagHasKey(this.image, "url")) + if (this.image && bagHasKey(this.image, "url")) { this._resolveImageLink(); + } - this._resetBagMembersToRawText([this.searchLists.subtitle, - this.searchLists.title]); + this._resetBagMembersToRawText([this.searchLists.subtitle, this.searchLists.title]); }, - _calcEnclosureCountAndFeedType: function Feed_calcEnclosureCountAndFeedType() { + _calcEnclosureCountAndFeedType: function () { var entries_with_enclosures = 0; var audio_count = 0; var image_count = 0; @@ -295,14 +304,18 @@ Feed.prototype = { if (/^audio/.test(enctype)) { ++audio_count; - } else if (/^image/.test(enctype)) { + } + else if (/^image/.test(enctype)) { ++image_count; - } else if (/^video/.test(enctype)) { + } + else if (/^video/.test(enctype)) { ++video_count; - } else { + } + else { ++other_count; } - } else { + } + else { ++other_count; } } @@ -322,9 +335,11 @@ Feed.prototype = { 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) { + } + else if (image_count > 0 && !audio_count && !video_count) { feedtype = Ci.nsIFeed.TYPE_IMAGE; - } else if (video_count > 0 && !audio_count && !image_count) { + } + else if (video_count > 0 && !audio_count && !image_count) { feedtype = Ci.nsIFeed.TYPE_VIDEO; } } @@ -333,33 +348,37 @@ Feed.prototype = { this.enclosureCount = other_count + video_count + audio_count + image_count; }, - _atomLinksToURI: function Feed_linkToURI() { + _atomLinksToURI: function () { 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")) + if (bagHasKey(alternates[0], "xml:base")) { base = alternates[0].getPropertyAsAString("xml:base"); + } this.link = this._resolveURI(href, base); } }, - _resolveImageLink: function Feed_resolveImageLink() { + _resolveImageLink: function () { var base; - if (bagHasKey(this.image, "xml:base")) + if (bagHasKey(this.image, "xml:base")) { base = this.image.getPropertyAsAString("xml:base"); + } var url = this._resolveURI(this.image.getPropertyAsAString("url"), base); - if (url) + if (url) { this.image.setPropertyAsAString("url", url.spec); + } }, - _resolveURI: function Feed_resolveURI(linkSpec, baseSpec) { + _resolveURI: function (linkSpec, baseSpec) { var uri = null; try { var base = baseSpec ? strToURI(baseSpec, this.baseURI) : this.baseURI; uri = strToURI(linkSpec, base); - } catch (e) { + } + catch (e) { LOG(e); } @@ -367,13 +386,12 @@ Feed.prototype = { }, // reset the bag to raw contents, not text constructs - _resetBagMembersToRawText: function Feed_resetBagMembers(fieldLists) { + _resetBagMembersToRawText: function (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); + this.fields.setPropertyAsAString(fieldLists[i][j], textConstruct.text); } } } @@ -388,8 +406,7 @@ function Entry() { this.summary = null; this.content = null; this.title = null; - this.fields = Cc["@mozilla.org/hash-property-bag;1"]. - createInstance(Ci.nsIWritablePropertyBag2); + this.fields = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2); this.link = null; this.id = null; this.baseURI = null; @@ -434,12 +451,13 @@ Entry.prototype = { ], }, - normalize: function Entry_normalize() { + normalize: function () { fieldsToObj(this, this.searchLists); // Assign Atom link if needed - if (bagHasKey(this.fields, "links")) + if (bagHasKey(this.fields, "links")) { this._atomLinksToURI(); + } // Populate enclosures array this._populateEnclosures(); @@ -449,54 +467,67 @@ Entry.prototype = { var guid = this.fields.getProperty("guid"); var isPermaLink = true; - if (bagHasKey(guid, "isPermaLink")) + if (bagHasKey(guid, "isPermaLink")) { isPermaLink = guid.getProperty("isPermaLink").toLowerCase() != "false"; + } - if (guid && isPermaLink) + if (guid && isPermaLink) { this.link = strToURI(guid.getProperty("guid")); + } } - if (this.updated) + if (this.updated) { this.updated = dateParse(this.updated); - if (this.published) + } + if (this.published) { this.published = dateParse(this.published); + } - this._resetBagMembersToRawText([this.searchLists.content, - this.searchLists.summary, - this.searchLists.title]); + this._resetBagMembersToRawText([ + this.searchLists.content, + this.searchLists.summary, + this.searchLists.title, + ]); }, - _populateEnclosures: function Entry_populateEnclosures() { - if (bagHasKey(this.fields, "links")) + _populateEnclosures: function () { + if (bagHasKey(this.fields, "links")) { this._atomLinksToEnclosures(); + } // Add RSS2 enclosure to enclosures - if (bagHasKey(this.fields, "enclosure")) + if (bagHasKey(this.fields, "enclosure")) { this._enclosureToEnclosures(); + } // Add media:content to enclosures - if (bagHasKey(this.fields, "mediacontent")) + if (bagHasKey(this.fields, "mediacontent")) { this._mediaToEnclosures("mediacontent"); + } // Add media:thumbnail to enclosures - if (bagHasKey(this.fields, "mediathumbnail")) + if (bagHasKey(this.fields, "mediathumbnail")) { this._mediaToEnclosures("mediathumbnail"); + } // Add media:content in media:group to enclosures - if (bagHasKey(this.fields, "mediagroup")) + if (bagHasKey(this.fields, "mediagroup")) { this._mediaToEnclosures("mediagroup", "mediacontent"); + } }, __enclosure_map: null, - _addToEnclosures: function Entry_addToEnclosures(new_enc) { + _addToEnclosures: function (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") == "") + if (!bagHasKey(new_enc, "url") || new_enc.getPropertyAsAString("url") == "") { return; + } - if (this.__enclosure_map == null) + if (this.__enclosure_map == null) { this.__enclosure_map = {}; + } var previous_enc = this.__enclosure_map[new_enc.getPropertyAsAString("url")]; @@ -510,11 +541,13 @@ Entry.prototype = { if (handlerInfoWrapper && handlerInfoWrapper.description) { previous_enc.setPropertyAsAString("typeDesc", handlerInfoWrapper.description); } - } catch (ext) {} + } + catch (ext) {} } - if (!bagHasKey(previous_enc, "length") && bagHasKey(new_enc, "length")) + if (!bagHasKey(previous_enc, "length") && bagHasKey(new_enc, "length")) { previous_enc.setPropertyAsAString("length", new_enc.getPropertyAsAString("length")); + } return; } @@ -528,42 +561,47 @@ Entry.prototype = { this.__enclosure_map[new_enc.getPropertyAsAString("url")] = new_enc; }, - _atomLinksToEnclosures: function Entry_linkToEnclosure() { + _atomLinksToEnclosures: function () { var links = this.fields.getPropertyAsInterface("links", Ci.nsIArray); var enc_links = findAtomLinks("enclosure", links); - if (enc_links.length == 0) + 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"))) + 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")) + if (bagHasKey(link, "type")) { enc.setPropertyAsAString("type", link.getPropertyAsAString("type")); - if (bagHasKey(link, "length")) + } + if (bagHasKey(link, "length")) { enc.setPropertyAsAString("length", link.getPropertyAsAString("length")); + } this._addToEnclosures(enc); } }, - _enclosureToEnclosures: function Entry_enclosureToEnclosures() { + _enclosureToEnclosures: function () { var enc = this.fields.getPropertyAsInterface("enclosure", Ci.nsIPropertyBag2); - if (!(enc.getProperty("url"))) + if (!(enc.getProperty("url"))) { return; + } this._addToEnclosures(enc); }, - _mediaToEnclosures: function Entry_mediaToEnclosures(mediaType, contentType) { + _mediaToEnclosures: function (mediaType, contentType) { var content; // If a contentType is specified, the mediaType is a simple propertybag, @@ -571,7 +609,8 @@ Entry.prototype = { if (contentType) { var group = this.fields.getPropertyAsInterface(mediaType, Ci.nsIPropertyBag2); content = group.getPropertyAsInterface(contentType, Ci.nsIArray); - } else { + } + else { content = this.fields.getPropertyAsInterface(mediaType, Ci.nsIArray); } @@ -580,8 +619,9 @@ Entry.prototype = { // media:content don't require url, but if it's not there, we should // skip it. - if (!bagHasKey(contentElement, "url")) + if (!bagHasKey(contentElement, "url")) { continue; + } var enc = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2); @@ -589,7 +629,8 @@ Entry.prototype = { enc.setPropertyAsAString("url", contentElement.getPropertyAsAString("url")); if (bagHasKey(contentElement, "type")) { enc.setPropertyAsAString("type", contentElement.getPropertyAsAString("type")); - } else if (mediaType == "mediathumbnail") { + } + else if (mediaType == "mediathumbnail") { // thumbnails won't have a type, but default to image types enc.setPropertyAsAString("type", "image/*"); enc.setPropertyAsBool("thumbnail", true); @@ -612,8 +653,7 @@ Entry.prototype = { Entry.prototype._atomLinksToURI = Feed.prototype._atomLinksToURI; Entry.prototype._resolveURI = Feed.prototype._resolveURI; -Entry.prototype._resetBagMembersToRawText = - Feed.prototype._resetBagMembersToRawText; +Entry.prototype._resetBagMembersToRawText = Feed.prototype._resetBagMembersToRawText; // TextConstruct represents and element that could contain (X)HTML function TextConstruct() { @@ -625,17 +665,17 @@ function TextConstruct() { } TextConstruct.prototype = { - plainText: function TC_plainText() { + plainText: function () { if (this.type != "text") { return this.parserUtils.convertToPlainText(stripTags(this.text), - Ci.nsIDocumentEncoder.OutputSelectionOnly | - Ci.nsIDocumentEncoder.OutputAbsoluteLinks, + Ci.nsIDocumentEncoder.OutputSelectionOnly + | Ci.nsIDocumentEncoder.OutputAbsoluteLinks, 0); } return this.text; }, - createDocumentFragment: function TC_createDocumentFragment(element) { + createDocumentFragment: function (element) { if (this.type == "text") { var doc = element.ownerDocument; var docFragment = doc.createDocumentFragment(); @@ -644,16 +684,19 @@ TextConstruct.prototype = { return docFragment; } var isXML; - if (this.type == "xhtml") + if (this.type == "xhtml") { isXML = true; - else if (this.type == "html") + } + else if (this.type == "html") { isXML = false; - else + } + else { return null; + } let flags = Ci.nsIParserUtils.SanitizerDropForms; return this.parserUtils.parseFragment(this.text, flags, isXML, - this.base, element); + this.base, element); }, // XPCOM stuff @@ -737,7 +780,8 @@ function fieldsToObj(container, fields) { field = isArray(props) ? props[0] : props; try { prop = container.fields.getProperty(field); - } catch (e) { + } + catch (e) { } if (prop) { prop = isArray(props) ? props[1](prop) : prop; @@ -801,27 +845,32 @@ function rssAuthor(s, author) { // 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})+$/; + 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) + if (match2.indexOf("mailto:") == 0) { match2 = match2.substring(7); + } if (emailCheck.test(match1)) { author.email = match1; author.name = match2; - } else if (emailCheck.test(match2)) { + } + else if (emailCheck.test(match2)) { author.email = match2; author.name = match1; - } else { + } + else { // put it back together author.name = match1 + " (" + match2 + ")"; } - } else { + } + else { author.name = chars; - if (chars.indexOf("@")) + if (chars.indexOf("@")) { author.email = chars; + } } return author; } @@ -831,8 +880,7 @@ function rssAuthor(s, author) { // 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); + var str = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); str.data = s; str.QueryInterface(Ci.nsISupportsString); return str; @@ -872,31 +920,33 @@ function XHTMLHandler(processor, isAtom) { // 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) { + // look back up at the declared namespaces + // we always use the same prefixes for our safe stuff + _isInScope: function (ns) { for (var i in this._inScopeNS) { for (var uri in this._inScopeNS[i]) { - if (this._inScopeNS[i][uri] == ns) + if (this._inScopeNS[i][uri] == ns) { return true; + } } } return false; }, - startDocument: function XH_startDocument() { + startDocument: function () { }, - endDocument: function XH_endDocument() { + endDocument: function () { }, - startElement: function XH_startElement(namespace, localName, qName, attributes) { + startElement: function (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") + 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) { @@ -906,9 +956,10 @@ XHTMLHandler.prototype = { uri = attributes.getURI(i); // XHTML attributes aren't in a namespace if (uri == "") { - this._buf += (" " + attributes.getLocalName(i) + "='" + - xmlEscape(attributes.getValue(i)) + "'"); - } else { + 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) { @@ -917,9 +968,9 @@ XHTMLHandler.prototype = { // it's an allowed attribute NS. // write the attribute - this._buf += (" " + prefix + ":" + - attributes.getLocalName(i) + - "='" + attributeValue + "'"); + this._buf += (" " + prefix + ":" + + attributes.getLocalName(i) + + "='" + attributeValue + "'"); // write an xmlns declaration if necessary if (prefix != "xml" && !this._isInScope(uri)) { @@ -932,18 +983,18 @@ XHTMLHandler.prototype = { this._buf += ">"; } }, - endElement: function XH_endElement(uri, localName, qName) { + endElement: function (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") + 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); + this._processor.returnFromXHTMLHandler(this._buf.trim(), uri, localName, qName); return; } // If it's an XHTML element, record it. Otherwise, it's ignored. @@ -951,10 +1002,10 @@ XHTMLHandler.prototype = { this._buf += ""; } }, - characters: function XH_characters(data) { + characters: function (data) { this._buf += xmlEscape(data); }, - processingInstruction: function XH_processingInstruction() { + processingInstruction: function () { }, }; @@ -978,11 +1029,11 @@ function ExtensionHandler(processor) { } ExtensionHandler.prototype = { - startDocument: function EH_startDocument() { + startDocument: function () { }, - endDocument: function EH_endDocument() { + endDocument: function () { }, - startElement: function EH_startElement(uri, localName, qName, attrs) { + startElement: function (uri, localName, qName, attrs) { ++this._depth; if (this._depth == 1) { @@ -995,19 +1046,19 @@ ExtensionHandler.prototype = { // if we descend into another element, we won't send text this._hasChildElements = (this._depth > 1); }, - endElement: function EH_endElement(uri, localName, qName) { + endElement: function (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); + this._processor.returnFromExtHandler(this._uri, this._localName, text, this._attrs); } }, - characters: function EH_characters(data) { - if (!this._hasChildElements) + characters: function (data) { + if (!this._hasChildElements) { this._buf += data; + } }, - processingInstruction: function EH_processingInstruction() { + processingInstruction: function () { }, }; @@ -1048,7 +1099,7 @@ function WrapperElementInfo(fieldName) { /** *** The Processor *****/ function FeedProcessor() { this._reader = Cc[SAX_CONTRACTID].createInstance(Ci.nsISAXXMLReader); - this._buf = ""; + this._buf = ""; this._feed = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2); this._handlerStack = []; this._xmlBaseStack = []; // sparse array keyed to nesting depth @@ -1199,7 +1250,7 @@ function FeedProcessor() { FeedProcessor.prototype = { // Set ourselves as the SAX handler, and set the base URI - _init: function FP_init(uri) { + _init: function (uri) { this._reader.contentHandler = this; this._reader.errorHandler = this; this._result = Cc[FR_CONTRACTID].createInstance(Ci.nsIFeedResult); @@ -1213,36 +1264,40 @@ FeedProcessor.prototype = { // 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) { + _docVerified: function (version) { this._result.doc = Cc[FEED_CONTRACTID].createInstance(Ci.nsIFeed); - this._result.doc.baseURI = - this._xmlBaseStack[this._xmlBaseStack.length - 1]; + 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() { + _sendResult: function () { this._haveSentResult = true; try { // Can be null when a non-feed is fed to us - if (this._result.doc) + if (this._result.doc) { this._result.doc.normalize(); - } catch (e) { + } + } + catch (e) { LOG("FIXME: " + e); } try { - if (this.listener != null) + if (this.listener != null) { this.listener.handleResult(this._result); - } finally { + } + } + finally { this._result = null; } }, // Parsing functions - parseAsync: function FP_parseAsync(requestObserver, uri) { + parseAsync: function (requestObserver, uri) { this._init(uri); this._reader.parseAsync(requestObserver); }, @@ -1251,23 +1306,23 @@ FeedProcessor.prototype = { // The XMLReader will throw sensible exceptions if these get called // out of order. - onStartRequest: function FP_onStartRequest(request, context) { + onStartRequest: function (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) { + onStopRequest: function (request, context, statusCode) { try { this._reader.onStopRequest(request, context, statusCode); - } finally { + } + finally { this._reader = null; } }, - onDataAvailable: - function FP_onDataAvailable(request, context, inputStream, offset, count) { + onDataAvailable: function (request, context, inputStream, offset, count) { this._reader.onDataAvailable(request, context, inputStream, offset, count); }, @@ -1278,22 +1333,24 @@ FeedProcessor.prototype = { // 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() { + fatalError: function () { this._result.bozo = true; // XXX need to QI to FeedProgressListener - if (!this._haveSentResult) + if (!this._haveSentResult) { this._sendResult(); + } }, // nsISAXContentHandler - startDocument: function FP_startDocument() { + startDocument: function () { // LOG("----------"); }, - endDocument: function FP_endDocument() { - if (!this._haveSentResult) + endDocument: function () { + if (!this._haveSentResult) { this._sendResult(); + } }, // The transitions defined above identify elements that contain more @@ -1323,7 +1380,7 @@ FeedProcessor.prototype = { // formats allow this by default, and I don't of any extension that // works this way. // - startElement: function FP_startElement(uri, localName, qName, attributes) { + startElement: function (uri, localName, qName, attributes) { this._buf = ""; ++this._depth; var elementInfo; @@ -1333,8 +1390,8 @@ FeedProcessor.prototype = { // Check for xml:base var base = attributes.getValueFromName(XMLNS, "base"); if (base) { - this._xmlBaseStack[this._depth] = - strToURI(base, this._xmlBaseStack[this._xmlBaseStack.length - 1]); + this._xmlBaseStack[this._depth] + = strToURI(base, this._xmlBaseStack[this._xmlBaseStack.length - 1]); } // To identify the element we're dealing with, we look up the @@ -1343,7 +1400,7 @@ FeedProcessor.prototype = { // 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; + 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: @@ -1362,12 +1419,12 @@ FeedProcessor.prototype = { // 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) { + 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._xhtmlHandler + = new XHTMLHandler(this, (this._result.version == "atom")); this._reader.contentHandler = this._xhtmlHandler; return; } @@ -1378,7 +1435,8 @@ FeedProcessor.prototype = { // 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 { + } + else { // If we don't have a transition, hand off to extension handler this._extensionHandler = new ExtensionHandler(this); this._reader.contentHandler = this._extensionHandler; @@ -1392,19 +1450,23 @@ FeedProcessor.prototype = { if (elementInfo.isWrapper) { this._state = "IN_" + elementInfo.fieldName.toUpperCase(); this._stack.push([this._feed, this._state]); - } else if (elementInfo.feedVersion) { + } + else if (elementInfo.feedVersion) { this._state = "IN_" + elementInfo.fieldName.toUpperCase(); // Check for the older RSS2 variants - if (elementInfo.feedVersion == "rss2") + if (elementInfo.feedVersion == "rss2") { elementInfo.feedVersion = this._findRSSVersion(attributes); - else if (uri == RSS090NS) + } + else if (uri == RSS090NS) { elementInfo.feedVersion = "rss090"; + } this._docVerified(elementInfo.feedVersion); this._stack.push([this._feed, this._state]); this._mapAttributes(this._feed, attributes); - } else { + } + else { this._state = this._processComplexElement(elementInfo, attributes); } }, @@ -1414,34 +1476,38 @@ FeedProcessor.prototype = { // 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) { + endElement: function (uri, localName, qName) { var elementInfo = this._handlerStack[this._depth]; // LOG(""); - if (elementInfo && !elementInfo.isWrapper) + if (elementInfo && !elementInfo.isWrapper) { this._closeComplexElement(elementInfo); + } // cut down xml:base context - if (this._xmlBaseStack.length == this._depth + 1) + 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) + 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) { + characters: function (data) { this._buf += data; }, - processingInstruction: function FP_processingInstruction(target, data) { + processingInstruction: function (target, data) { if (target == "xml-stylesheet") { var hrefAttribute = data.match(/href=[\"\'](.*?)[\"\']/); - if (hrefAttribute && hrefAttribute.length == 2) + if (hrefAttribute && hrefAttribute.length == 2) { this._result.stylesheet = strToURI(hrefAttribute[1], this._result.uri); + } } }, @@ -1449,8 +1515,7 @@ FeedProcessor.prototype = { // Handle our more complicated elements--those that contain // attributes and child elements. - _processComplexElement: - function FP__processComplexElement(elementInfo, attributes) { + _processComplexElement: function (elementInfo, attributes) { var obj; // If the container is an entry/item, it'll need to have its @@ -1459,11 +1524,13 @@ FeedProcessor.prototype = { obj = elementInfo.containerClass.createInstance(Ci.nsIFeedEntry); obj.baseURI = this._xmlBaseStack[this._xmlBaseStack.length - 1]; this._mapAttributes(obj.fields, attributes); - } else if (elementInfo.containerClass) { + } + 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 { + } + else { obj = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2); this._mapAttributes(obj, attributes); } @@ -1480,14 +1547,16 @@ FeedProcessor.prototype = { var prop; try { prop = container.getProperty(elementInfo.fieldName); - } catch (e) { + } + catch (e) { } if (elementInfo.isArray) { if (!prop) { - container.setPropertyAsInterface(elementInfo.fieldName, - Cc[ARRAY_CONTRACTID]. - createInstance(Ci.nsIMutableArray)); + container.setPropertyAsInterface( + elementInfo.fieldName, + Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray), + ); } newProp = container.getProperty(elementInfo.fieldName); @@ -1499,9 +1568,11 @@ FeedProcessor.prototype = { // If new object is an nsIFeedContainer, we want to deal with // its member nsIPropertyBag instead. - if (isIFeedContainer(obj)) + if (isIFeedContainer(obj)) { newProp = obj.fields; - } else { + } + } + else { // If it doesn't, set it. if (!prop) { container.setPropertyAsInterface(elementInfo.fieldName, obj); @@ -1519,7 +1590,7 @@ FeedProcessor.prototype = { // 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) { + _closeComplexElement: function (elementInfo) { var stateTuple = this._stack.pop(); var container = stateTuple[0]; var containerParent = stateTuple[2]; @@ -1528,37 +1599,45 @@ FeedProcessor.prototype = { // If it's an array and we have to post-process, // grab the last element - if (isArray) + if (isArray) { element = container.queryElementAt(container.length - 1, Ci.nsISupports); - else + } + else { element = container; + } // Run the post-processing function if there is one. - if (elementInfo.closeFunc) + 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]) + if (elementInfo.containerClass == Cc[ENTRY_CONTRACTID]) { containerParent.normalize(); + } // If it's an array, re-set the last element - if (isArray) + if (isArray) { container.replaceElementAt(element, container.length - 1); + } }, - _prefixForNS: function FP_prefixForNS(uri) { - if (!uri) + _prefixForNS: function (uri) { + if (!uri) { return ""; + } var prefix = gNamespaces[uri]; - if (prefix) + if (prefix) { return prefix + ":"; - if (uri.toLowerCase().indexOf("http://backend.userland.com") == 0) + } + if (uri.toLowerCase().indexOf("http://backend.userland.com") == 0) { return ""; + } return null; }, - _mapAttributes: function FP__mapAttributes(bag, attributes) { + _mapAttributes: function (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) { @@ -1569,38 +1648,44 @@ FeedProcessor.prototype = { }, // Only for RSS2esque formats - _findRSSVersion: function FP__findRSSVersion(attributes) { + _findRSSVersion: function (attributes) { var versionAttr = attributes.getValueFromName("", "version").trim(); - var versions = { "0.91": "rss091", - "0.92": "rss092", - "0.93": "rss093", - "0.94": "rss094" }; - if (versions[versionAttr]) + 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.") + } + 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) { + returnFromExtHandler: function (uri, localName, chars, attributes) { --this._depth; // take control of the SAX events this._reader.contentHandler = this; - if (localName == null && chars == null) + if (localName == null && chars == null) { return; + } // we don't take random elements inside rdf:RDF - if (this._state == "IN_RDF") + if (this._state == "IN_RDF") { return; + } // Grab the top of the stack var top = this._stack[this._stack.length - 1]; - if (!top) + if (!top) { return; + } var container = top[0]; // Grab the last element if it's an array @@ -1609,23 +1694,28 @@ FeedProcessor.prototype = { // 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); + Ci.nsIFeedElementBase); // XXX there must be a way to flatten these interfaces - if (contract == Cc[PERSON_CONTRACTID]) + if (contract == Cc[PERSON_CONTRACTID]) { el.QueryInterface(Ci.nsIFeedPerson); - else + } + 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")) + if ( + (uri == "" + || prefix + && ((prefix.indexOf("atom") > -1) + || (prefix.indexOf("rss") > -1))) + && (propName == "url" || propName == "href") + ) { propName = "uri"; + } try { if (el[propName] !== "undefined") { @@ -1637,14 +1727,14 @@ FeedProcessor.prototype = { } el[propName] = propValue; } - } catch (e) { + } + 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); + container = container.queryElementAt(container.length - 1, Ci.nsIWritablePropertyBag2); } // Make the buffer our new property @@ -1652,35 +1742,38 @@ FeedProcessor.prototype = { // 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); + 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) { + } + else if (this._result.version == "atom03" && typeAttribute != null) { if (typeAttribute.toLowerCase().includes("xhtml")) { type = "xhtml"; - } else if (typeAttribute.toLowerCase().includes("html")) { + } + else if (typeAttribute.toLowerCase().includes("html")) { type = "html"; - } else if (typeAttribute.toLowerCase().includes("text")) { + } + 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) { + 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 { + } + else { container.setPropertyAsAString(propName, chars); } }, @@ -1689,25 +1782,23 @@ FeedProcessor.prototype = { // (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) { + returnFromXHTMLHandler: function (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) + if (!top) { return; + } var container = top[0]; // Assign the property - var newProp = newProp = Cc[TEXTCONSTRUCT_CONTRACTID]. - createInstance(Ci.nsIFeedTextConstruct); + 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); + 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 From e120ca7395b99081465635624872b994fefb713c Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Sat, 8 May 2021 17:24:04 +0100 Subject: [PATCH 08/21] Align variable style in feed processor This tweaks variable naming and related details to match Zotero conventions. --- resource/feeds/FeedProcessor.js | 94 ++++++++++++++++----------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/resource/feeds/FeedProcessor.js b/resource/feeds/FeedProcessor.js index 67654cbde3..96cb01e43e 100644 --- a/resource/feeds/FeedProcessor.js +++ b/resource/feeds/FeedProcessor.js @@ -284,18 +284,18 @@ Feed.prototype = { }, _calcEnclosureCountAndFeedType: function () { - var entries_with_enclosures = 0; - var audio_count = 0; - var image_count = 0; - var video_count = 0; - var other_count = 0; + var entriesWithEnclosures = 0; + var audioCount = 0; + var imageCount = 0; + var videoCount = 0; + var otherCount = 0; for (var i = 0; i < this.items.length; ++i) { var entry = this.items.queryElementAt(i, Ci.nsIFeedEntry); entry.QueryInterface(Ci.nsIFeedContainer); if (entry.enclosures && entry.enclosures.length > 0) { - ++entries_with_enclosures; + ++entriesWithEnclosures; for (var e = 0; e < entry.enclosures.length; ++e) { var enc = entry.enclosures.queryElementAt(e, Ci.nsIWritablePropertyBag2); @@ -303,20 +303,20 @@ Feed.prototype = { var enctype = enc.get("type"); if (/^audio/.test(enctype)) { - ++audio_count; + ++audioCount; } else if (/^image/.test(enctype)) { - ++image_count; + ++imageCount; } else if (/^video/.test(enctype)) { - ++video_count; + ++videoCount; } else { - ++other_count; + ++otherCount; } } else { - ++other_count; + ++otherCount; } } } @@ -332,20 +332,20 @@ Feed.prototype = { // 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) { + if (entriesWithEnclosures == this.items.length && otherCount == 0) { + if (audioCount > 0 && !videoCount && !imageCount) { feedtype = Ci.nsIFeed.TYPE_AUDIO; } - else if (image_count > 0 && !audio_count && !video_count) { + else if (imageCount > 0 && !audioCount && !videoCount) { feedtype = Ci.nsIFeed.TYPE_IMAGE; } - else if (video_count > 0 && !audio_count && !image_count) { + else if (videoCount > 0 && !audioCount && !imageCount) { feedtype = Ci.nsIFeed.TYPE_VIDEO; } } this.type = feedtype; - this.enclosureCount = other_count + video_count + audio_count + image_count; + this.enclosureCount = otherCount + videoCount + audioCount + imageCount; }, _atomLinksToURI: function () { @@ -516,60 +516,60 @@ Entry.prototype = { } }, - __enclosure_map: null, + __enclosureMap: null, - _addToEnclosures: function (new_enc) { + _addToEnclosures: function (newEnc) { // 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") == "") { + if (!bagHasKey(newEnc, "url") || newEnc.getPropertyAsAString("url") == "") { return; } - if (this.__enclosure_map == null) { - this.__enclosure_map = {}; + if (this.__enclosureMap === null) { + this.__enclosureMap = {}; } - var previous_enc = this.__enclosure_map[new_enc.getPropertyAsAString("url")]; + var previousEnc = this.__enclosureMap[newEnc.getPropertyAsAString("url")]; - if (previous_enc != undefined) { - previous_enc.QueryInterface(Ci.nsIWritablePropertyBag2); + if (previousEnc != undefined) { + previousEnc.QueryInterface(Ci.nsIWritablePropertyBag2); - if (!bagHasKey(previous_enc, "type") && bagHasKey(new_enc, "type")) { - previous_enc.setPropertyAsAString("type", new_enc.getPropertyAsAString("type")); + if (!bagHasKey(previousEnc, "type") && bagHasKey(newEnc, "type")) { + previousEnc.setPropertyAsAString("type", newEnc.getPropertyAsAString("type")); try { - let handlerInfoWrapper = gMimeService.getFromTypeAndExtension(new_enc.getPropertyAsAString("type"), null); + let handlerInfoWrapper = gMimeService.getFromTypeAndExtension(newEnc.getPropertyAsAString("type"), null); if (handlerInfoWrapper && handlerInfoWrapper.description) { - previous_enc.setPropertyAsAString("typeDesc", handlerInfoWrapper.description); + previousEnc.setPropertyAsAString("typeDesc", handlerInfoWrapper.description); } } catch (ext) {} } - if (!bagHasKey(previous_enc, "length") && bagHasKey(new_enc, "length")) { - previous_enc.setPropertyAsAString("length", new_enc.getPropertyAsAString("length")); + if (!bagHasKey(previousEnc, "length") && bagHasKey(newEnc, "length")) { + previousEnc.setPropertyAsAString("length", newEnc.getPropertyAsAString("length")); } return; } - if (this.enclosures == null) { + 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; + this.enclosures.appendElement(newEnc); + this.__enclosureMap[newEnc.getPropertyAsAString("url")] = newEnc; }, _atomLinksToEnclosures: function () { var links = this.fields.getPropertyAsInterface("links", Ci.nsIArray); - var enc_links = findAtomLinks("enclosure", links); - if (enc_links.length == 0) { + var encLinks = findAtomLinks("enclosure", links); + if (encLinks.length == 0) { return; } - for (var i = 0; i < enc_links.length; ++i) { - var link = enc_links[i]; + for (var i = 0; i < encLinks.length; ++i) { + var link = encLinks[i]; // an enclosure must have an href if (!(link.getProperty("href"))) { @@ -846,7 +846,7 @@ function rssAuthor(s, author) { var chars = s.trim(); var matches = chars.match(/(.*)\((.*)\)/); var emailCheck - = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/; + = /^([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(); @@ -962,7 +962,7 @@ XHTMLHandler.prototype = { else { // write a small set of allowed attribute namespaces var prefix = gAllowedXHTMLNamespaces[uri]; - if (prefix != null) { + if (prefix) { // The attribute value we'll attempt to write var attributeValue = xmlEscape(attributes.getValue(i)); @@ -1287,7 +1287,7 @@ FeedProcessor.prototype = { } try { - if (this.listener != null) { + if (this.listener !== null) { this.listener.handleResult(this._result); } } @@ -1420,9 +1420,9 @@ FeedProcessor.prototype = { // and explicitly allows whitespace collapsing. // if ((this._result.version == "atom" || this._result.version == "atom03") - && this._textConstructs[key] != null) { + && this._textConstructs[key]) { var type = attributes.getValueFromName("", "type"); - if (type != null && type.includes("xhtml")) { + if (type !== null && type.includes("xhtml")) { this._xhtmlHandler = new XHTMLHandler(this, (this._result.version == "atom")); this._reader.contentHandler = this._xhtmlHandler; @@ -1504,7 +1504,7 @@ FeedProcessor.prototype = { processingInstruction: function (target, data) { if (target == "xml-stylesheet") { - var hrefAttribute = data.match(/href=[\"\'](.*?)[\"\']/); + var hrefAttribute = data.match(/href=["'](.*?)["']/); if (hrefAttribute && hrefAttribute.length == 2) { this._result.stylesheet = strToURI(hrefAttribute[1], this._result.uri); } @@ -1672,7 +1672,7 @@ FeedProcessor.prototype = { // take control of the SAX events this._reader.contentHandler = this; - if (localName == null && chars == null) { + if (localName === null && chars === null) { return; } @@ -1742,17 +1742,17 @@ FeedProcessor.prototype = { // But, it could be something containing HTML. If so, // we need to know about that. - if (this._textConstructs[propName] != null + if (this._textConstructs[propName] && 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) { + if (this._result.version == "atom" && typeAttribute !== null) { type = typeAttribute; } - else if (this._result.version == "atom03" && typeAttribute != null) { + else if (this._result.version == "atom03" && typeAttribute !== null) { if (typeAttribute.toLowerCase().includes("xhtml")) { type = "xhtml"; } From 9d0e1932dcbc000d73e1609b4261422862b4673c Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Sat, 8 May 2021 23:04:46 +0100 Subject: [PATCH 09/21] Convert feed processor structure away from XPCOM This changes the various components to invoke each other directly as regular objects without involving XPCOM. --- resource/feeds/FeedProcessor.js | 181 ++++++++++---------------------- 1 file changed, 56 insertions(+), 125 deletions(-) diff --git a/resource/feeds/FeedProcessor.js b/resource/feeds/FeedProcessor.js index 96cb01e43e..d090b62c52 100644 --- a/resource/feeds/FeedProcessor.js +++ b/resource/feeds/FeedProcessor.js @@ -4,38 +4,12 @@ /* eslint-disable quote-props */ +"use strict"; + function LOG(str) { - dump("*** " + str + "\n"); + Zotero.debug("Feed Processor: " + str); } -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"; @@ -190,6 +164,7 @@ var gAllowedXHTMLNamespaces = { "http://www.w3.org/1999/xhtml": "xhtml", }; +// Implements nsIFeedResult function FeedResult() {} FeedResult.prototype = { bozo: false, @@ -202,12 +177,9 @@ FeedResult.prototype = { registerExtensionPrefix: function (ns, prefix) { throw Cr.NS_ERROR_NOT_IMPLEMENTED; }, - - // XPCOM stuff - classID: FR_CLASSID, - QueryInterface: ChromeUtils.generateQI([Ci.nsIFeedResult]), }; +// Implements nsIFeed, nsIFeedContainer function Feed() { this.subtitle = null; this.title = null; @@ -396,12 +368,9 @@ Feed.prototype = { } } }, - - // XPCOM stuff - classID: FEED_CLASSID, - QueryInterface: ChromeUtils.generateQI([Ci.nsIFeed, Ci.nsIFeedContainer]), }; +// Implements nsIFeedEntry, nsIFeedContainer function Entry() { this.summary = null; this.content = null; @@ -643,12 +612,6 @@ Entry.prototype = { this._addToEnclosures(enc); } }, - - // XPCOM stuff - classID: ENTRY_CLASSID, - QueryInterface: ChromeUtils.generateQI( - [Ci.nsIFeedEntry, Ci.nsIFeedContainer] - ), }; Entry.prototype._atomLinksToURI = Feed.prototype._atomLinksToURI; @@ -656,6 +619,7 @@ Entry.prototype._resolveURI = Feed.prototype._resolveURI; Entry.prototype._resetBagMembersToRawText = Feed.prototype._resetBagMembersToRawText; // TextConstruct represents and element that could contain (X)HTML +// Implements nsIFeedTextConstruct function TextConstruct() { this.lang = null; this.base = null; @@ -698,13 +662,10 @@ TextConstruct.prototype = { 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 +// Implements nsIFeedGenerator, nsIFeedElementBase function Generator() { this.lang = null; this.agent = null; @@ -717,7 +678,6 @@ function Generator() { } Generator.prototype = { - get attributes() { return this._attributes; }, @@ -736,14 +696,9 @@ Generator.prototype = { this.uri = strToURI(uriAttribute, this.baseURI); } }, - - // XPCOM stuff - classID: GENERATOR_CLASSID, - QueryInterface: ChromeUtils.generateQI( - [Ci.nsIFeedGenerator, Ci.nsIFeedElementBase] - ), }; +// Implements nsIFeedPerson, nsIFeedElementBase function Person() { this.name = null; this.uri = null; @@ -754,14 +709,6 @@ function Person() { 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. * @@ -1046,7 +993,7 @@ ExtensionHandler.prototype = { // if we descend into another element, we won't send text this._hasChildElements = (this._depth > 1); }, - endElement: function (uri, localName, qName) { + endElement: function (_uri, _localName, _qName) { --this._depth; if (this._depth == 0) { var text = this._hasChildElements ? null : this._buf.trim(); @@ -1097,6 +1044,8 @@ function WrapperElementInfo(fieldName) { } /** *** The Processor *****/ +// Implements nsIFeedProcessor, nsISAXContentHandler, nsISAXErrorHandler, +// nsIStreamListener, nsIRequestObserver function FeedProcessor() { this._reader = Cc[SAX_CONTRACTID].createInstance(Ci.nsISAXXMLReader); this._buf = ""; @@ -1156,25 +1105,25 @@ function FeedProcessor() { }, "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), + "item": new ElementInfo("items", Entry, null, true), + "managingEditor": new ElementInfo("authors", Person, rssAuthor, true), + "dc:creator": new ElementInfo("authors", Person, rssAuthor, true), + "dc:author": new ElementInfo("authors", Person, rssAuthor, true), + "dc:contributor": new ElementInfo("contributors", Person, 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), + "generator": new ElementInfo("generator", Generator, 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), + "author": new ElementInfo("authors", Person, rssAuthor, true), + "dc:creator": new ElementInfo("authors", Person, rssAuthor, true), + "dc:author": new ElementInfo("authors", Person, rssAuthor, true), + "dc:contributor": new ElementInfo("contributors", Person, rssAuthor, true), "category": new ElementInfo("categories", null, rssCatTerm, true), "enclosure": new ElementInfo("enclosure", null, null, false), "media:content": new ElementInfo("mediacontent", null, null, true), @@ -1202,58 +1151,57 @@ function FeedProcessor() { "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), + "rss1:item": new ElementInfo("items", Entry, 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), + "admin:generatorAgent": new ElementInfo("generator", Generator, null, false), + "dc:creator": new ElementInfo("authors", Person, rssAuthor, true), + "dc:author": new ElementInfo("authors", Person, rssAuthor, true), + "dc:contributor": new ElementInfo("contributors", Person, 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:author": new ElementInfo("authors", Person, null, true), + "atom:generator": new ElementInfo("generator", Generator, atomGenerator, false), + "atom:contributor": new ElementInfo("contributors", Person, 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), + "atom:entry": new ElementInfo("entries", Entry, 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:author": new ElementInfo("authors", Person, null, true), + "atom:contributor": new ElementInfo("contributors", Person, 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:author": new ElementInfo("authors", Person, null, true), + "atom03:contributor": new ElementInfo("contributors", Person, 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), + "atom03:entry": new ElementInfo("atom03_entries", Entry, null, true), + "atom03:generator": new ElementInfo("generator", Generator, 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:author": new ElementInfo("authors", Person, null, true), + "atom03:contributor": new ElementInfo("contributors", Person, null, true), "atom03:link": new ElementInfo("links", null, null, true), - "atom03:entry": new ElementInfo("atom03_entries", Cc[ENTRY_CONTRACTID], null, true), + "atom03:entry": new ElementInfo("atom03_entries", Entry, 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 (uri) { this._reader.contentHandler = this; this._reader.errorHandler = this; - this._result = Cc[FR_CONTRACTID].createInstance(Ci.nsIFeedResult); + this._result = new FeedResult(); if (uri) { this._result.uri = uri; this._reader.baseURI = uri; @@ -1265,7 +1213,7 @@ FeedProcessor.prototype = { // we're dealing with. Some feed types require digging a bit further // than the root. _docVerified: function (version) { - this._result.doc = Cc[FEED_CONTRACTID].createInstance(Ci.nsIFeed); + this._result.doc = new Feed(); this._result.doc.baseURI = this._xmlBaseStack[this._xmlBaseStack.length - 1]; this._result.doc.fields = this._feed; @@ -1476,7 +1424,7 @@ FeedProcessor.prototype = { // 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 (uri, localName, qName) { + endElement: function (_uri, _localName, _qName) { var elementInfo = this._handlerStack[this._depth]; // LOG(""); if (elementInfo && !elementInfo.isWrapper) { @@ -1520,13 +1468,14 @@ FeedProcessor.prototype = { // 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); + const Class = elementInfo.containerClass; + if (Class == Entry) { + obj = new Class(); obj.baseURI = this._xmlBaseStack[this._xmlBaseStack.length - 1]; this._mapAttributes(obj.fields, attributes); } else if (elementInfo.containerClass) { - obj = elementInfo.containerClass.createInstance(Ci.nsIFeedElementBase); + obj = new Class(); obj.baseURI = this._xmlBaseStack[this._xmlBaseStack.length - 1]; obj.attributes = attributes; // just set the SAX attributes } @@ -1613,7 +1562,7 @@ FeedProcessor.prototype = { // If an nsIFeedContainer was on top of the stack, // we need to normalize it - if (elementInfo.containerClass == Cc[ENTRY_CONTRACTID]) { + if (elementInfo.containerClass == Entry) { containerParent.normalize(); } @@ -1692,11 +1641,11 @@ FeedProcessor.prototype = { 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]) { + if (contract && contract != Entry) { var el = container.queryElementAt(container.length - 1, Ci.nsIFeedElementBase); // XXX there must be a way to flatten these interfaces - if (contract == Cc[PERSON_CONTRACTID]) { + if (contract == Person) { el.QueryInterface(Ci.nsIFeedPerson); } else { @@ -1744,7 +1693,7 @@ FeedProcessor.prototype = { // we need to know about that. if (this._textConstructs[propName] && this._handlerStack[this._depth].containerClass !== null) { - var newProp = Cc[TEXTCONSTRUCT_CONTRACTID].createInstance(Ci.nsIFeedTextConstruct); + var newProp = new TextConstruct(); newProp.text = chars; // Look up the default type in our table var type = this._textConstructs[propName]; @@ -1766,7 +1715,7 @@ FeedProcessor.prototype = { // 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) { + && this._handlerStack[this._depth].containerClass != Entry) { type = "text"; } newProp.type = type; @@ -1794,7 +1743,7 @@ FeedProcessor.prototype = { var container = top[0]; // Assign the property - var newProp = newProp = Cc[TEXTCONSTRUCT_CONTRACTID].createInstance(Ci.nsIFeedTextConstruct); + var newProp = new TextConstruct(); newProp.text = chars; newProp.type = "xhtml"; newProp.base = this._xmlBaseStack[this._xmlBaseStack.length - 1]; @@ -1806,26 +1755,8 @@ FeedProcessor.prototype = { // 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); +if (typeof module == "object") { + module.exports = FeedProcessor; +} From bea3b3d3b2fd90f51bd91059d9c5ce154619ddbd Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Sat, 8 May 2021 23:06:53 +0100 Subject: [PATCH 10/21] Remove unimplemented registerExtensionPrefix API --- resource/feeds/FeedProcessor.js | 4 ---- resource/feeds/nsIFeedContainer.idl | 1 - resource/feeds/nsIFeedResult.idl | 5 ----- 3 files changed, 10 deletions(-) diff --git a/resource/feeds/FeedProcessor.js b/resource/feeds/FeedProcessor.js index d090b62c52..be226b29cc 100644 --- a/resource/feeds/FeedProcessor.js +++ b/resource/feeds/FeedProcessor.js @@ -173,10 +173,6 @@ FeedResult.prototype = { headers: null, uri: null, stylesheet: null, - - registerExtensionPrefix: function (ns, prefix) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - }, }; // Implements nsIFeed, nsIFeedContainer diff --git a/resource/feeds/nsIFeedContainer.idl b/resource/feeds/nsIFeedContainer.idl index d288035eef..00bc32cbfe 100644 --- a/resource/feeds/nsIFeedContainer.idl +++ b/resource/feeds/nsIFeedContainer.idl @@ -31,7 +31,6 @@ interface nsIFeedContainer : nsIFeedElementBase * and accessing specific fields. * * Common namespaces are accessed using prefixes, like get("dc:subject");. - * See nsIFeedResult::registerExtensionPrefix. */ attribute nsIWritablePropertyBag2 fields; diff --git a/resource/feeds/nsIFeedResult.idl b/resource/feeds/nsIFeedResult.idl index beb8420fe0..327d553d98 100644 --- a/resource/feeds/nsIFeedResult.idl +++ b/resource/feeds/nsIFeedResult.idl @@ -56,9 +56,4 @@ interface nsIFeedResult : nsISupports { * 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); }; From 75eb8c51d435d1cbdd486040d121a696f1a54e38 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Sat, 8 May 2021 23:45:24 +0100 Subject: [PATCH 11/21] Ignore feed processor *.idl files --- scripts/config.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/config.js b/scripts/config.js index db27240dc9..6cef46e04a 100644 --- a/scripts/config.js +++ b/scripts/config.js @@ -50,6 +50,8 @@ const symlinkFiles = [ 'resource/ace/theme-chrome.js', 'resource/ace/theme-monokai.js', 'resource/ace/worker-javascript.js', + // Feed *.idl files are for documentation only + '!resource/feeds/*.idl', 'update.rdf', '!chrome/skin/default/zotero/**/*.scss' ]; From be80598c6b1b118f9a2c971730c6b2ded3dd8dd0 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Sun, 9 May 2021 00:20:07 +0100 Subject: [PATCH 12/21] Convert feed processor data away from XPCOM This changes the feed processor XPCOM array and property bags to native arrays and objects. --- chrome/content/zotero/xpcom/feedReader.js | 26 +- resource/feeds/FeedProcessor.js | 274 ++++++++-------------- 2 files changed, 110 insertions(+), 190 deletions(-) diff --git a/chrome/content/zotero/xpcom/feedReader.js b/chrome/content/zotero/xpcom/feedReader.js index 239120015a..1c01001b3e 100644 --- a/chrome/content/zotero/xpcom/feedReader.js +++ b/chrome/content/zotero/xpcom/feedReader.js @@ -108,7 +108,7 @@ Zotero.FeedReader = function (url) { let items = this._feed.items; if (items && items.length) { for (let i = 0; i < items.length; i++) { - let item = items.queryElementAt(i, Components.interfaces.nsIFeedEntry); + let item = items[i]; if (!item) continue; let feedItem = Zotero.FeedReader._getFeedItem(item, this._feedProperties); @@ -295,7 +295,7 @@ Zotero.FeedReader._processCreators = function (feedEntry, field, role) { try { let personArr = feedEntry[field]; // Seems like this part can throw if there is no author data in the feed for (let i = 0; i < personArr.length; i++) { - let person = personArr.queryElementAt(i, Components.interfaces.nsIFeedPerson); + let person = personArr[i]; if (!person || !person.name) continue; let name = Zotero.Utilities.cleanTags(Zotero.Utilities.trimInternal(person.name)); @@ -321,8 +321,6 @@ Zotero.FeedReader._processCreators = function (feedEntry, field, role) { } } catch (e) { - if (e.result != Components.results.NS_ERROR_FAILURE) throw e; - if (field != 'authors') return []; // ieeexplore places these in "authors"... sigh @@ -502,18 +500,16 @@ let ns = { }; Zotero.FeedReader._getFeedField = function (feedEntry, field, namespace) { let prefix = namespace ? ns[namespace] || 'null' : ''; - try { - return feedEntry.fields.getPropertyAsAUTF8String(prefix + field); + if (feedEntry.fields[prefix + field]) { + return feedEntry.fields[prefix + field]; } - catch (e) {} - try { - if (namespace && !ns[namespace]) { - prefix = namespace + ':'; - return feedEntry.fields.getPropertyAsAUTF8String(prefix + field); + if (namespace && !ns[namespace]) { + prefix = namespace + ':'; + if (feedEntry.fields[prefix + field]) { + return feedEntry.fields[prefix + field]; } } - catch (e) { } return null; }; @@ -523,9 +519,9 @@ Zotero.FeedReader._getEnclosedItems = function (feedEntry) { if (feedEntry.enclosures) { for (let i = 0; i < feedEntry.enclosures.length; i++) { - let elem = feedEntry.enclosures.queryElementAt(0, Components.interfaces.nsIPropertyBag2); - if (elem.get('url')) { - let enclosedItem = { url: elem.get('url'), contentType: elem.get('type') || '' }; + let elem = feedEntry.enclosures[0]; + if (elem.url) { + let enclosedItem = { url: elem.url, contentType: elem.type || '' }; enclosedItems.push(enclosedItem); } } diff --git a/resource/feeds/FeedProcessor.js b/resource/feeds/FeedProcessor.js index be226b29cc..adbaccfd90 100644 --- a/resource/feeds/FeedProcessor.js +++ b/resource/feeds/FeedProcessor.js @@ -10,8 +10,6 @@ function LOG(str) { Zotero.debug("Feed Processor: " + str); } -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"; @@ -74,12 +72,12 @@ 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); + var linkElement = links[i]; // atom:link MUST have @href - if (bagHasKey(linkElement, "href")) { + if (linkElement.href) { var relAttribute = null; - if (bagHasKey(linkElement, "rel")) { - relAttribute = linkElement.getPropertyAsAString("rel"); + if (linkElement.rel) { + relAttribute = linkElement.rel; } if ((!relAttribute && rel == "alternate") || relAttribute == rel) { rvLinks.push(linkElement); @@ -103,34 +101,9 @@ function xmlEscape(s) { return s; } -function arrayContains(array, element) { - for (var i = 0; i < array.length; ++i) { - if (array[i] == element) { - return true; - } - } - return false; -} - -// XXX add hasKey to nsIPropertyBag -function bagHasKey(bag, key) { - try { - bag.getProperty(key); - return true; - } - catch (e) { - return false; - } -} - function makePropGetter(key) { - return function (bag) { - try { - return bag.getProperty(key); - } - catch (e) { - } - return null; + return function(bag) { + return bag[key]; }; } @@ -179,12 +152,12 @@ FeedResult.prototype = { function Feed() { this.subtitle = null; this.title = null; - this.items = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray); + this.items = []; this.link = null; this.id = null; this.generator = null; - this.authors = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray); - this.contributors = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray); + this.authors = []; + this.contributors = []; this.baseURI = null; this.enclosureCount = 0; this.type = Ci.nsIFeed.TYPE_FEED; @@ -226,10 +199,10 @@ Feed.prototype = { normalize: function () { fieldsToObj(this, this.searchLists); if (this.skipDays) { - this.skipDays = this.skipDays.getProperty("days"); + this.skipDays = this.skipDays.days; } if (this.skipHours) { - this.skipHours = this.skipHours.getProperty("hours"); + this.skipHours = this.skipHours.hours; } if (this.updated) { @@ -237,14 +210,14 @@ Feed.prototype = { } // Assign Atom link if needed - if (bagHasKey(this.fields, "links")) { + if (this.fields.links) { this._atomLinksToURI(); } this._calcEnclosureCountAndFeedType(); // Resolve relative image links - if (this.image && bagHasKey(this.image, "url")) { + if (this.image && this.image.url) { this._resolveImageLink(); } @@ -259,16 +232,15 @@ Feed.prototype = { var otherCount = 0; for (var i = 0; i < this.items.length; ++i) { - var entry = this.items.queryElementAt(i, Ci.nsIFeedEntry); - entry.QueryInterface(Ci.nsIFeedContainer); + var entry = this.items[i]; if (entry.enclosures && entry.enclosures.length > 0) { ++entriesWithEnclosures; 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"); + var enc = entry.enclosures[e]; + if (enc.type) { + var enctype = enc.type; if (/^audio/.test(enctype)) { ++audioCount; @@ -317,13 +289,13 @@ Feed.prototype = { }, _atomLinksToURI: function () { - var links = this.fields.getPropertyAsInterface("links", Ci.nsIArray); + var links = this.fields.links; var alternates = findAtomLinks("alternate", links); if (alternates.length > 0) { - var href = alternates[0].getPropertyAsAString("href"); + var href = alternates[0].href; var base; - if (bagHasKey(alternates[0], "xml:base")) { - base = alternates[0].getPropertyAsAString("xml:base"); + if (alternates[0]["xml:base"]) { + base = alternates[0]["xml:base"]; } this.link = this._resolveURI(href, base); } @@ -331,12 +303,12 @@ Feed.prototype = { _resolveImageLink: function () { var base; - if (bagHasKey(this.image, "xml:base")) { - base = this.image.getPropertyAsAString("xml:base"); + if (this.image["xml:base"]) { + base = this.image["xml:base"]; } - var url = this._resolveURI(this.image.getPropertyAsAString("url"), base); + var url = this._resolveURI(this.image.url, base); if (url) { - this.image.setPropertyAsAString("url", url.spec); + this.image.url = url.spec; } }, @@ -357,9 +329,9 @@ Feed.prototype = { _resetBagMembersToRawText: function (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); + if (this.fields[fieldLists[i][j]]) { + var textConstruct = this.fields[fieldLists[i][j]]; + this.fields[fieldLists[i][j]] = textConstruct.text; } } } @@ -371,14 +343,14 @@ function Entry() { this.summary = null; this.content = null; this.title = null; - this.fields = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2); + this.fields = {}; 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); + this.authors = []; + this.contributors = []; } Entry.prototype = { @@ -420,7 +392,7 @@ Entry.prototype = { fieldsToObj(this, this.searchLists); // Assign Atom link if needed - if (bagHasKey(this.fields, "links")) { + if (this.fields.links) { this._atomLinksToURI(); } @@ -428,16 +400,16 @@ Entry.prototype = { this._populateEnclosures(); // The link might be a guid w/ permalink=true - if (!this.link && bagHasKey(this.fields, "guid")) { - var guid = this.fields.getProperty("guid"); + if (!this.link && this.fields.guid) { + var guid = this.fields.guid; var isPermaLink = true; - if (bagHasKey(guid, "isPermaLink")) { - isPermaLink = guid.getProperty("isPermaLink").toLowerCase() != "false"; + if (guid.isPermaLink) { + isPermaLink = guid.isPermaLink.toLowerCase() != "false"; } if (guid && isPermaLink) { - this.link = strToURI(guid.getProperty("guid")); + this.link = strToURI(guid.guid); } } @@ -456,27 +428,27 @@ Entry.prototype = { }, _populateEnclosures: function () { - if (bagHasKey(this.fields, "links")) { + if (this.fields.links) { this._atomLinksToEnclosures(); } // Add RSS2 enclosure to enclosures - if (bagHasKey(this.fields, "enclosure")) { + if (this.fields.enclosure) { this._enclosureToEnclosures(); } // Add media:content to enclosures - if (bagHasKey(this.fields, "mediacontent")) { + if (this.fields.mediacontent) { this._mediaToEnclosures("mediacontent"); } // Add media:thumbnail to enclosures - if (bagHasKey(this.fields, "mediathumbnail")) { + if (this.fields.mediathumbnail) { this._mediaToEnclosures("mediathumbnail"); } // Add media:content in media:group to enclosures - if (bagHasKey(this.fields, "mediagroup")) { + if (this.fields.mediagroup) { this._mediaToEnclosures("mediagroup", "mediacontent"); } }, @@ -486,7 +458,7 @@ Entry.prototype = { _addToEnclosures: function (newEnc) { // items we add to the enclosures array get displayed in the FeedWriter and // they must have non-empty urls. - if (!bagHasKey(newEnc, "url") || newEnc.getPropertyAsAString("url") == "") { + if (!newEnc.url || newEnc.url == "") { return; } @@ -494,40 +466,37 @@ Entry.prototype = { this.__enclosureMap = {}; } - var previousEnc = this.__enclosureMap[newEnc.getPropertyAsAString("url")]; + var previousEnc = this.__enclosureMap[newEnc.url]; if (previousEnc != undefined) { - previousEnc.QueryInterface(Ci.nsIWritablePropertyBag2); - - if (!bagHasKey(previousEnc, "type") && bagHasKey(newEnc, "type")) { - previousEnc.setPropertyAsAString("type", newEnc.getPropertyAsAString("type")); + if (!previousEnc.type && newEnc.type) { + previousEnc.type = newEnc.type; try { - let handlerInfoWrapper = gMimeService.getFromTypeAndExtension(newEnc.getPropertyAsAString("type"), null); + let handlerInfoWrapper = gMimeService.getFromTypeAndExtension(newEnc.type, null); if (handlerInfoWrapper && handlerInfoWrapper.description) { - previousEnc.setPropertyAsAString("typeDesc", handlerInfoWrapper.description); + previousEnc.typeDesc = handlerInfoWrapper.description; } } catch (ext) {} } - if (!bagHasKey(previousEnc, "length") && bagHasKey(newEnc, "length")) { - previousEnc.setPropertyAsAString("length", newEnc.getPropertyAsAString("length")); + if (!previousEnc.length && newEnc.length) { + previousEnc.length = newEnc.length; } return; } if (this.enclosures === null) { - this.enclosures = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray); - this.enclosures.QueryInterface(Ci.nsIMutableArray); + this.enclosures = []; } - this.enclosures.appendElement(newEnc); - this.__enclosureMap[newEnc.getPropertyAsAString("url")] = newEnc; + this.enclosures.push(newEnc); + this.__enclosureMap[newEnc.url] = newEnc; }, _atomLinksToEnclosures: function () { - var links = this.fields.getPropertyAsInterface("links", Ci.nsIArray); + var links = this.fields.links; var encLinks = findAtomLinks("enclosure", links); if (encLinks.length == 0) { return; @@ -537,19 +506,19 @@ Entry.prototype = { var link = encLinks[i]; // an enclosure must have an href - if (!(link.getProperty("href"))) { + if (!link.href) { return; } - var enc = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2); + var enc = {}; // copy Atom bits over to equivalent enclosure bits - enc.setPropertyAsAString("url", link.getPropertyAsAString("href")); - if (bagHasKey(link, "type")) { - enc.setPropertyAsAString("type", link.getPropertyAsAString("type")); + enc.url = link.href; + if (link.type) { + enc.type = link.type; } - if (bagHasKey(link, "length")) { - enc.setPropertyAsAString("length", link.getPropertyAsAString("length")); + if (link.length) { + enc.length = link.length; } this._addToEnclosures(enc); @@ -557,9 +526,9 @@ Entry.prototype = { }, _enclosureToEnclosures: function () { - var enc = this.fields.getPropertyAsInterface("enclosure", Ci.nsIPropertyBag2); + var enc = this.fields.enclosure; - if (!(enc.getProperty("url"))) { + if (!enc.url) { return; } @@ -572,37 +541,37 @@ Entry.prototype = { // 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); + var group = this.fields[mediaType]; + content = group[contentType]; } else { - content = this.fields.getPropertyAsInterface(mediaType, Ci.nsIArray); + content = this.fields[mediaType]; } for (var i = 0; i < content.length; ++i) { - var contentElement = content.queryElementAt(i, Ci.nsIWritablePropertyBag2); + var contentElement = content[i]; // media:content don't require url, but if it's not there, we should // skip it. - if (!bagHasKey(contentElement, "url")) { + if (!contentElement.url) { continue; } - var enc = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2); + var enc = {}; // 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")); + enc.url = contentElement.url; + if (contentElement.type) { + enc.type = contentElement.type; } else if (mediaType == "mediathumbnail") { // thumbnails won't have a type, but default to image types - enc.setPropertyAsAString("type", "image/*"); - enc.setPropertyAsBool("thumbnail", true); + enc.type = "image/*"; + enc.thumbnail = true; } - if (bagHasKey(contentElement, "fileSize")) { - enc.setPropertyAsAString("length", contentElement.getPropertyAsAString("fileSize")); + if (contentElement.fileSize) { + enc.length = contentElement.fileSize; } this._addToEnclosures(enc); @@ -721,11 +690,7 @@ function fieldsToObj(container, fields) { props = searchList[i]; prop = null; field = isArray(props) ? props[0] : props; - try { - prop = container.fields.getProperty(field); - } - catch (e) { - } + prop = container.fields[field]; if (prop) { prop = isArray(props) ? props[1](prop) : prop; container[key] = prop; @@ -734,39 +699,27 @@ function fieldsToObj(container, fields) { } } -/** - * 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()); + logo.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()); + cat.term = s.trim(); return cat; } // post-process a GUID function rssGuid(s, guid) { - guid.setPropertyAsAString("guid", s.trim()); + guid.guid = s.trim(); return guid; } @@ -784,7 +737,6 @@ function rssGuid(s, guid) { // fields. // function rssAuthor(s, author) { - author.QueryInterface(Ci.nsIFeedPerson); // check for RSS2 string format var chars = s.trim(); var matches = chars.match(/(.*)\((.*)\)/); @@ -818,17 +770,6 @@ function rssAuthor(s, author) { 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 @@ -1045,7 +986,7 @@ function WrapperElementInfo(fieldName) { function FeedProcessor() { this._reader = Cc[SAX_CONTRACTID].createInstance(Ci.nsISAXXMLReader); this._buf = ""; - this._feed = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2); + this._feed = {}; this._handlerStack = []; this._xmlBaseStack = []; // sparse array keyed to nesting depth this._depth = 0; @@ -1129,11 +1070,11 @@ function FeedProcessor() { }, "IN_SKIPDAYS": { - "day": new ElementInfo("days", null, rssArrayElement, true), + "day": new ElementInfo("days", null, null, true), }, "IN_SKIPHOURS": { - "hour": new ElementInfo("hours", null, rssArrayElement, true), + "hour": new ElementInfo("hours", null, null, true), }, "IN_MEDIAGROUP": { @@ -1476,7 +1417,7 @@ FeedProcessor.prototype = { obj.attributes = attributes; // just set the SAX attributes } else { - obj = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2); + obj = {}; this._mapAttributes(obj, attributes); } @@ -1489,27 +1430,15 @@ FeedProcessor.prototype = { 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) { - } + var prop = container[elementInfo.fieldName]; if (elementInfo.isArray) { if (!prop) { - container.setPropertyAsInterface( - elementInfo.fieldName, - Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray), - ); + container[elementInfo.fieldName] = []; } - 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); + newProp = container[elementInfo.fieldName]; + newProp.push(obj); // If new object is an nsIFeedContainer, we want to deal with // its member nsIPropertyBag instead. @@ -1520,9 +1449,9 @@ FeedProcessor.prototype = { else { // If it doesn't, set it. if (!prop) { - container.setPropertyAsInterface(elementInfo.fieldName, obj); + container[elementInfo.fieldName] = obj; } - newProp = container.getProperty(elementInfo.fieldName); + newProp = container[elementInfo.fieldName]; } // make our new state name, and push the property onto the stack @@ -1545,7 +1474,7 @@ FeedProcessor.prototype = { // 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); + element = container[container.length - 1]; } else { element = container; @@ -1564,7 +1493,7 @@ FeedProcessor.prototype = { // If it's an array, re-set the last element if (isArray) { - container.replaceElementAt(element, container.length - 1); + container[container.length - 1] = element; } }, @@ -1588,7 +1517,7 @@ FeedProcessor.prototype = { 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); + bag[key] = val; } }, @@ -1638,13 +1567,8 @@ FeedProcessor.prototype = { var contract = this._handlerStack[this._depth].containerClass; // check if it's something specific, but not an entry if (contract && contract != Entry) { - var el = container.queryElementAt(container.length - 1, - Ci.nsIFeedElementBase); - // XXX there must be a way to flatten these interfaces - if (contract == Person) { - el.QueryInterface(Ci.nsIFeedPerson); - } - else { + var el = container[container.length - 1]; + if (contract != Person) { return; // don't know about this interface } @@ -1679,7 +1603,7 @@ FeedProcessor.prototype = { // the rest of the function deals with entry- and feed-level stuff return; } - container = container.queryElementAt(container.length - 1, Ci.nsIWritablePropertyBag2); + container = container[container.length - 1]; } // Make the buffer our new property @@ -1716,10 +1640,10 @@ FeedProcessor.prototype = { } newProp.type = type; newProp.base = this._xmlBaseStack[this._xmlBaseStack.length - 1]; - container.setPropertyAsInterface(propName, newProp); + container[propName] = newProp; } else { - container.setPropertyAsAString(propName, chars); + container[propName] = chars; } }, @@ -1743,7 +1667,7 @@ FeedProcessor.prototype = { newProp.text = chars; newProp.type = "xhtml"; newProp.base = this._xmlBaseStack[this._xmlBaseStack.length - 1]; - container.setPropertyAsInterface(this._prefixForNS(uri) + localName, newProp); + container[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 From af6029b85464e5ab7d648088fdca0f36534627a9 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 11 May 2021 10:12:48 +0100 Subject: [PATCH 13/21] Convert feed processor to native URLs --- chrome/content/zotero/xpcom/feedReader.js | 4 ++-- resource/feeds/FeedProcessor.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/chrome/content/zotero/xpcom/feedReader.js b/chrome/content/zotero/xpcom/feedReader.js index 1c01001b3e..6a9d1abc23 100644 --- a/chrome/content/zotero/xpcom/feedReader.js +++ b/chrome/content/zotero/xpcom/feedReader.js @@ -366,7 +366,7 @@ Zotero.FeedReader._getFeedItem = function (feedEntry, feedInfo) { } let item = { - guid: feedEntry.id || feedEntry.link.spec + guid: feedEntry.id || feedEntry.link.href }; if (feedEntry.title) item.title = Zotero.FeedReader._getRichText(feedEntry.title, 'title'); @@ -386,7 +386,7 @@ Zotero.FeedReader._getFeedItem = function (feedEntry, feedInfo) { } } - if (feedEntry.link) item.url = feedEntry.link.spec; + if (feedEntry.link) item.url = feedEntry.link.href; if (feedEntry.rights) item.rights = Zotero.FeedReader._getRichText(feedEntry.rights, 'rights'); diff --git a/resource/feeds/FeedProcessor.js b/resource/feeds/FeedProcessor.js index adbaccfd90..067333dab7 100644 --- a/resource/feeds/FeedProcessor.js +++ b/resource/feeds/FeedProcessor.js @@ -20,9 +20,9 @@ const RSS090NS = "http://my.netscape.com/rdf/simple/0.9/"; /** *** Some general utils *****/ function strToURI(link, base) { - base = base || null; + base = base || undefined; try { - return Services.io.newURI(link, null, base); + return new URL(link, base); } catch (e) { return null; @@ -308,7 +308,7 @@ Feed.prototype = { } var url = this._resolveURI(this.image.url, base); if (url) { - this.image.url = url.spec; + this.image.url = url.href; } }, From a3e6f9628930a6b790b8c808088969c1974244ed Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 11 May 2021 10:23:08 +0100 Subject: [PATCH 14/21] Remove MIME type description feature from feed processor This removes the feed processor's feature of converting enclosure MIME types to a textual type description. This is not used by Zotero and removes an XPCOM dependency. --- resource/feeds/FeedProcessor.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/resource/feeds/FeedProcessor.js b/resource/feeds/FeedProcessor.js index 067333dab7..f19017656f 100644 --- a/resource/feeds/FeedProcessor.js +++ b/resource/feeds/FeedProcessor.js @@ -13,8 +13,6 @@ function LOG(str) { 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/"; @@ -471,13 +469,6 @@ Entry.prototype = { if (previousEnc != undefined) { if (!previousEnc.type && newEnc.type) { previousEnc.type = newEnc.type; - try { - let handlerInfoWrapper = gMimeService.getFromTypeAndExtension(newEnc.type, null); - if (handlerInfoWrapper && handlerInfoWrapper.description) { - previousEnc.typeDesc = handlerInfoWrapper.description; - } - } - catch (ext) {} } if (!previousEnc.length && newEnc.length) { From a0694cd8fb3daa820a91f1a39e82d97eafbdd338 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 11 May 2021 10:33:09 +0100 Subject: [PATCH 15/21] Convert feed processor type checks away from XPCOM This converts the type checks for arrays and interfaces to use native JS approaches instead of XPCOM. --- resource/feeds/FeedProcessor.js | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/resource/feeds/FeedProcessor.js b/resource/feeds/FeedProcessor.js index f19017656f..8ab0ab4caa 100644 --- a/resource/feeds/FeedProcessor.js +++ b/resource/feeds/FeedProcessor.js @@ -39,25 +39,6 @@ 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, ""); } @@ -1433,7 +1414,7 @@ FeedProcessor.prototype = { // If new object is an nsIFeedContainer, we want to deal with // its member nsIPropertyBag instead. - if (isIFeedContainer(obj)) { + if (obj.fields) { newProp = obj.fields; } } @@ -1460,11 +1441,10 @@ FeedProcessor.prototype = { 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) { + if (isArray(container)) { element = container[container.length - 1]; } else { @@ -1483,7 +1463,7 @@ FeedProcessor.prototype = { } // If it's an array, re-set the last element - if (isArray) { + if (isArray(container)) { container[container.length - 1] = element; } }, @@ -1554,7 +1534,7 @@ FeedProcessor.prototype = { var container = top[0]; // Grab the last element if it's an array - if (isIArray(container)) { + if (isArray(container)) { var contract = this._handlerStack[this._depth].containerClass; // check if it's something specific, but not an entry if (contract && contract != Entry) { From b4303eaa45c6252a14e37a654738440ab6da92a7 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 11 May 2021 10:37:33 +0100 Subject: [PATCH 16/21] Absorb feed processor types from interface This moves the type constants into code, rather than pulling them from an XPCOM interface. --- resource/feeds/FeedProcessor.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/resource/feeds/FeedProcessor.js b/resource/feeds/FeedProcessor.js index 8ab0ab4caa..dc58a737fc 100644 --- a/resource/feeds/FeedProcessor.js +++ b/resource/feeds/FeedProcessor.js @@ -139,9 +139,14 @@ function Feed() { this.contributors = []; this.baseURI = null; this.enclosureCount = 0; - this.type = Ci.nsIFeed.TYPE_FEED; + this.type = Feed.TYPE_FEED; } +Feed.TYPE_FEED = 0; +Feed.TYPE_AUDIO = 1; +Feed.TYPE_IMAGE = 2; +Feed.TYPE_VIDEO = 4; + Feed.prototype = { searchLists: { title: ["title", "rss1:title", "atom03:title", "atom:title"], @@ -241,7 +246,7 @@ Feed.prototype = { } } - var feedtype = Ci.nsIFeed.TYPE_FEED; + var feedtype = Feed.TYPE_FEED; // For a feed to be marked as TYPE_VIDEO, TYPE_AUDIO and TYPE_IMAGE, // we enforce two things: @@ -253,13 +258,13 @@ Feed.prototype = { // Otherwise it's a TYPE_FEED. if (entriesWithEnclosures == this.items.length && otherCount == 0) { if (audioCount > 0 && !videoCount && !imageCount) { - feedtype = Ci.nsIFeed.TYPE_AUDIO; + feedtype = Feed.TYPE_AUDIO; } else if (imageCount > 0 && !audioCount && !videoCount) { - feedtype = Ci.nsIFeed.TYPE_IMAGE; + feedtype = Feed.TYPE_IMAGE; } else if (videoCount > 0 && !audioCount && !imageCount) { - feedtype = Ci.nsIFeed.TYPE_VIDEO; + feedtype = Feed.TYPE_VIDEO; } } From 891ba599a6dc16e4aa9d900f79f62558e4080af4 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 13 May 2021 21:31:08 +0100 Subject: [PATCH 17/21] Convert feed processor parsing to native DOM APIs This uses native DOM APIs to create document fragments and parse content instead of XPCOM utils. --- resource/feeds/FeedProcessor.js | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/resource/feeds/FeedProcessor.js b/resource/feeds/FeedProcessor.js index dc58a737fc..fa926fdebe 100644 --- a/resource/feeds/FeedProcessor.js +++ b/resource/feeds/FeedProcessor.js @@ -11,7 +11,6 @@ function LOG(str) { } const SAX_CONTRACTID = "@mozilla.org/saxparser/xmlreader;1"; -const PARSERUTILS_CONTRACTID = "@mozilla.org/parserutils;1"; const XMLNS = "http://www.w3.org/XML/1998/namespace"; const RSS090NS = "http://my.netscape.com/rdf/simple/0.9/"; @@ -567,42 +566,38 @@ function TextConstruct() { this.base = null; this.type = "text"; this.text = null; - this.parserUtils = Cc[PARSERUTILS_CONTRACTID].getService(Ci.nsIParserUtils); } TextConstruct.prototype = { plainText: function () { if (this.type != "text") { - return this.parserUtils.convertToPlainText(stripTags(this.text), - Ci.nsIDocumentEncoder.OutputSelectionOnly - | Ci.nsIDocumentEncoder.OutputAbsoluteLinks, - 0); + return stripTags(this.text); } return this.text; }, createDocumentFragment: function (element) { if (this.type == "text") { - var doc = element.ownerDocument; - var docFragment = doc.createDocumentFragment(); - var node = doc.createTextNode(this.text); + const doc = element.ownerDocument; + const docFragment = doc.createDocumentFragment(); + const node = doc.createTextNode(this.text); docFragment.appendChild(node); return docFragment; } - var isXML; + + let parserType; if (this.type == "xhtml") { - isXML = true; + parserType = "application/xhtml+xml"; } else if (this.type == "html") { - isXML = false; + parserType = "text/html"; } else { return null; } - let flags = Ci.nsIParserUtils.SanitizerDropForms; - return this.parserUtils.parseFragment(this.text, flags, isXML, - this.base, element); + const parsedDoc = new DOMParser().parseFromString(this.text, parserType); + return parsedDoc.documentElement; }, }; From 1d93296481344002554593de36ecaa81d2976358 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 20 May 2021 01:14:01 +0100 Subject: [PATCH 18/21] Convert feed processor to HTML attribute manipulation This moves the feed processor away from custom XPCOM SAX attributes and over to standard HTML attribute map APIs. --- resource/feeds/FeedProcessor.js | 36 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/resource/feeds/FeedProcessor.js b/resource/feeds/FeedProcessor.js index fa926fdebe..b7f059f483 100644 --- a/resource/feeds/FeedProcessor.js +++ b/resource/feeds/FeedProcessor.js @@ -621,13 +621,13 @@ Generator.prototype = { set attributes(value) { this._attributes = value; - this.version = this._attributes.getValueFromName("", "version"); - var uriAttribute = this._attributes.getValueFromName("", "uri") - || this._attributes.getValueFromName("", "url"); + this.version = (this._attributes.getNamedItemNS("", "version") || {}).value; + var uriAttribute = (this._attributes.getNamedItemNS("", "uri") || {}).value + || (this._attributes.getNamedItemNS("", "url") || {}).value; this.uri = strToURI(uriAttribute, this.baseURI); // RSS1 - uriAttribute = this._attributes.getValueFromName(RDF_NS, "resource"); + uriAttribute = (this._attributes.getNamedItemNS(RDF_NS, "resource") || {}).value; if (uriAttribute) { this.agent = uriAttribute; this.uri = strToURI(uriAttribute, this.baseURI); @@ -809,23 +809,23 @@ XHTMLHandler.prototype = { this._buf += "<" + localName; var uri; for (var i = 0; i < attributes.length; ++i) { - uri = attributes.getURI(i); + uri = attributes.item(i).namespaceURI; // XHTML attributes aren't in a namespace if (uri == "") { - this._buf += (" " + attributes.getLocalName(i) + "='" - + xmlEscape(attributes.getValue(i)) + "'"); + this._buf += (" " + attributes.item(i).localName + "='" + + xmlEscape(attributes.item(i).value) + "'"); } else { // write a small set of allowed attribute namespaces var prefix = gAllowedXHTMLNamespaces[uri]; if (prefix) { // The attribute value we'll attempt to write - var attributeValue = xmlEscape(attributes.getValue(i)); + var attributeValue = xmlEscape(attributes.item(i).value); // it's an allowed attribute NS. // write the attribute this._buf += (" " + prefix + ":" - + attributes.getLocalName(i) + + attributes.item(i).localName + "='" + attributeValue + "'"); // write an xmlns declaration if necessary @@ -1245,7 +1245,7 @@ FeedProcessor.prototype = { // LOG("<" + localName + ">"); // Check for xml:base - var base = attributes.getValueFromName(XMLNS, "base"); + var base = (attributes.getNamedItemNS(XMLNS, "base") || {}).value; if (base) { this._xmlBaseStack[this._depth] = strToURI(base, this._xmlBaseStack[this._xmlBaseStack.length - 1]); @@ -1278,8 +1278,8 @@ FeedProcessor.prototype = { // if ((this._result.version == "atom" || this._result.version == "atom03") && this._textConstructs[key]) { - var type = attributes.getValueFromName("", "type"); - if (type !== null && type.includes("xhtml")) { + var type = (attributes.getNamedItemNS("", "type") || {}).value; + if (type && type.includes("xhtml")) { this._xhtmlHandler = new XHTMLHandler(this, (this._result.version == "atom")); this._reader.contentHandler = this._xhtmlHandler; @@ -1486,15 +1486,15 @@ FeedProcessor.prototype = { // 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); + var key = this._prefixForNS(attributes.item(i).namespaceURI) + attributes.item(i).localName; + var val = attributes.item(i).value; bag[key] = val; } }, // Only for RSS2esque formats _findRSSVersion: function (attributes) { - var versionAttr = attributes.getValueFromName("", "version").trim(); + var versionAttr = (attributes.getNamedItemNS("", "version") || {}).value.trim(); var versions = { "0.91": "rss091", "0.92": "rss092", @@ -1588,11 +1588,11 @@ FeedProcessor.prototype = { 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) { + var typeAttribute = (attributes.getNamedItemNS("", "type") || {}).value; + if (this._result.version == "atom" && typeAttribute) { type = typeAttribute; } - else if (this._result.version == "atom03" && typeAttribute !== null) { + else if (this._result.version == "atom03" && typeAttribute) { if (typeAttribute.toLowerCase().includes("xhtml")) { type = "xhtml"; } From 29ebaa5ec62e751f7197b2931a4a0f8061ae20f4 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 21 May 2021 17:53:21 +0100 Subject: [PATCH 19/21] Assume fetch-based API for feed processsing This converts the feed processor to expect a `fetch`-based API instead of an XPCOM stream. --- resource/feeds/FeedProcessor.js | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/resource/feeds/FeedProcessor.js b/resource/feeds/FeedProcessor.js index b7f059f483..be065e9722 100644 --- a/resource/feeds/FeedProcessor.js +++ b/resource/feeds/FeedProcessor.js @@ -1159,28 +1159,10 @@ FeedProcessor.prototype = { this._reader.parseAsync(requestObserver); }, - // nsIStreamListener + // Fetch API - // The XMLReader will throw sensible exceptions if these get called - // out of order. - onStartRequest: function (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 (request, context, statusCode) { - try { - this._reader.onStopRequest(request, context, statusCode); - } - finally { - this._reader = null; - } - }, - - onDataAvailable: function (request, context, inputStream, offset, count) { - this._reader.onDataAvailable(request, context, inputStream, offset, count); + onResponseAvailable(response) { + return this._reader.onResponseAvailable(response); }, // nsISAXErrorHandler From 2bd4904ead9c4a604311b06c51ce8840f368168d Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 21 May 2021 18:01:50 +0100 Subject: [PATCH 20/21] Add DOMParser-based XML reader This adds a `DOMParser`-based XML reader which emits events like a SAX XML reader for compatibility with the feed processor. --- resource/feeds/FeedProcessor.js | 5 +- resource/feeds/SAXXMLReader.js | 140 ++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 resource/feeds/SAXXMLReader.js diff --git a/resource/feeds/FeedProcessor.js b/resource/feeds/FeedProcessor.js index be065e9722..96a6427d8b 100644 --- a/resource/feeds/FeedProcessor.js +++ b/resource/feeds/FeedProcessor.js @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* eslint-disable quote-props */ +/* globals SAXXMLReader */ "use strict"; @@ -10,8 +11,6 @@ function LOG(str) { Zotero.debug("Feed Processor: " + str); } -const SAX_CONTRACTID = "@mozilla.org/saxparser/xmlreader;1"; - const XMLNS = "http://www.w3.org/XML/1998/namespace"; const RSS090NS = "http://my.netscape.com/rdf/simple/0.9/"; @@ -956,7 +955,7 @@ function WrapperElementInfo(fieldName) { // Implements nsIFeedProcessor, nsISAXContentHandler, nsISAXErrorHandler, // nsIStreamListener, nsIRequestObserver function FeedProcessor() { - this._reader = Cc[SAX_CONTRACTID].createInstance(Ci.nsISAXXMLReader); + this._reader = new SAXXMLReader(); this._buf = ""; this._feed = {}; this._handlerStack = []; diff --git a/resource/feeds/SAXXMLReader.js b/resource/feeds/SAXXMLReader.js new file mode 100644 index 0000000000..b1cf4413eb --- /dev/null +++ b/resource/feeds/SAXXMLReader.js @@ -0,0 +1,140 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2021 Corporation for Digital Scholarship + Vienna, Virginia, USA + https://www.zotero.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see . + + ***** END LICENSE BLOCK ***** +*/ + +"use strict"; + +/** + * This implements `nsISAXXMLReader` using content-accessible APIs, such as `DOMParser` and + * `TreeWalker`. It should be usable in any web platform environment that supports those standard + * APIs. + * + * Note that while this class implements a SAX-style API (which usually implies streaming style + * parsing for documents of any length), this class actually uses whole document parsing internally. + * Instead, `DOMParser` reads the entire document and this walks the resulting DOM. Thus, this class + * is mainly useful only for smaller documents where it's useful to conform to SAX-style API to + * support existing code. + * + * Higher-level components are notified of XML content via the `nsISAXContentHandler` and + * `nsISAXErrorHandler` interfaces as this reader walks through the XML content. + */ +class SAXXMLReader { + constructor() { + this.contentHandler = null; + this.errorHandler = null; + this.baseURI = null; + this._data = null; + this._walker = null; + } + + // nsISAXXMLReader + + parseAsync(requestObserver) { + if (requestObserver) { + throw new Error("requestObserver argument parseAsync is not currently supported"); + } + } + + // Fetch API + + async onResponseAvailable(response) { + if (!response.ok) { + throw new Error("Unable to fetch data"); + } + this._data = await response.text(); + this._parseAndNotify(); + } + + // Parsing and notification + + _parseAndNotify() { + if (!this.contentHandler) { + return; + } + + const doc = new DOMParser().parseFromString(this._data, "text/xml"); + this._walker = doc.createTreeWalker(doc.documentElement); + + this.contentHandler.startDocument(); + this._walk(); + this.contentHandler.endDocument(); + + this._data = null; + this._walker = null; + } + + _walk() { + const node = this._walker.currentNode; + + switch (node.nodeType) { + // ELEMENT_NODE + case 1: { + this.contentHandler.startElement( + node.namespaceURI, + node.localName, + "", // qualifed names are not used + node.attributes, + ); + + // Try to move down + if (this._walker.firstChild()) { + this._walk(); + // Move up + this._walker.parentNode(); + } + + this.contentHandler.endElement( + node.namespaceURI, + node.localName, + "", // qualifed names are not used + ); + break; + } + // TEXT_NODE + case 3: { + this.contentHandler.characters(node.data); + break; + } + // CDATA_SECTION_NODE + case 4: { + this.contentHandler.characters(node.data); + break; + } + // PROCESSING_INSTRUCTION_NODE + case 7: { + this.contentHandler.processingInstruction(node.target, node.data); + break; + } + } + + // Try to move across + if (this._walker.nextSibling()) { + this._walk(); + } + } +} + +if (typeof module == "object") { + module.exports = SAXXMLReader; +} From eaa01901ef2f360e767ff2a9541eae6725658d66 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Sat, 22 May 2021 18:44:42 +0100 Subject: [PATCH 21/21] Wire up new feed processor to Zotero's feed reader This connects the new feed processor to Zotero's existing feed reader. The new feed processor assumes a content window environment, so a sandbox in a parent window is used to load it. --- chrome/content/zotero/xpcom/feedReader.js | 71 +++++++++++++++++------ test/tests/feedReaderTest.js | 14 ++++- test/tests/feedTest.js | 8 ++- 3 files changed, 72 insertions(+), 21 deletions(-) diff --git a/chrome/content/zotero/xpcom/feedReader.js b/chrome/content/zotero/xpcom/feedReader.js index 6a9d1abc23..2326d7e385 100644 --- a/chrome/content/zotero/xpcom/feedReader.js +++ b/chrome/content/zotero/xpcom/feedReader.js @@ -130,11 +130,49 @@ Zotero.FeedReader = function (url) { lastItem.resolve(null); }.bind(this)); - // Set up asynchronous feed processor - let feedProcessor = Components.classes["@mozilla.org/feed-processor;1"] - .createInstance(Components.interfaces.nsIFeedProcessor); + // The feed processor and related modules assume a content window environment, so we'll simulate + // one via a sandbox in a parent window. You might think we could jump straight to + // `hiddenDOMWindow` as a parent window, since it does indeed exist on all platforms... + // However, when loading scripts into the `hiddenDOMWindow` on Windows and Linux, they get + // stuck in some lazily parsed state which bizarrely drops function prototypes. To avoid this, + // we prefer other parent windows first, which work fine on all platforms. + let parentWindow = Services.wm.getMostRecentWindow("navigator:browser"); + if (!parentWindow) { + parentWindow = Services.ww.activeWindow; + } + // Use the hidden DOM window on macOS with the main window closed + if (!parentWindow) { + parentWindow = Services.appShell.hiddenDOMWindow; + } + if (!parentWindow) { + this.terminate("Parent window not available for feed reader"); + return; + } - let feedUrl = Services.io.newURI(url, null, null); + const sandbox = new Cu.Sandbox(parentWindow, { + sandboxPrototype: parentWindow, + sandboxName: "Feed Processor", + }); + sandbox.Zotero = { + debug: Components.utils.exportFunction(Zotero.debug, sandbox), + }; + + Services.scriptloader.loadSubScript("resource://zotero/feeds/FeedProcessor.js", sandbox); + Services.scriptloader.loadSubScript("resource://zotero/feeds/SAXXMLReader.js", sandbox); + + // Set up asynchronous feed processor + const { FeedProcessor } = sandbox; + const feedProcessor = new FeedProcessor(); + if (!feedProcessor.parseAsync) { + this.terminate("Feed processor failed to load in parent window"); + return; + } + + // Borrow web utils to fetch feed content + const { fetch, URL } = parentWindow; + + // Pass along the URL + const feedUrl = new URL(url); feedProcessor.parseAsync(null, feedUrl); /* @@ -148,19 +186,21 @@ Zotero.FeedReader = function (url) { this.terminate("No Feed"); return; } - - let newFeed = result.doc.QueryInterface(Components.interfaces.nsIFeed); - feedFetched.resolve(newFeed); + feedFetched.resolve(result.doc); } }; - Zotero.debug("FeedReader: Fetching feed from " + feedUrl.spec); + Zotero.debug("FeedReader: Fetching feed from " + feedUrl); - this._channel = Services.io.newChannelFromURI2(feedUrl, null, - Services.scriptSecurityManager.getSystemPrincipal(), null, - Ci.nsILoadInfo.SEC_NORMAL, Ci.nsIContentPolicy.TYPE_OTHER); - this._channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE; - this._channel.asyncOpen(feedProcessor, null); // Sends an HTTP request + // Fetch and start processing + fetch(feedUrl, { + cache: "no-store", + }).then((response) => { + return feedProcessor.onResponseAvailable(response); + }).catch((e) => { + Zotero.debug(e); + this.terminate("Processing failed"); + }); }; /* @@ -195,11 +235,6 @@ Zotero.FeedReader.prototype.terminate = function (status) { er.handledRejection = true; lastItem.reject(er); } - - // Close feed connection - if (this._channel.isPending()) { - this._channel.cancel(Components.results.NS_BINDING_ABORTED); - } }; Zotero.defineProperty(Zotero.FeedReader.prototype, 'feedProperties', { diff --git a/test/tests/feedReaderTest.js b/test/tests/feedReaderTest.js index 7b34a3efc2..402c0675a7 100644 --- a/test/tests/feedReaderTest.js +++ b/test/tests/feedReaderTest.js @@ -35,8 +35,18 @@ describe("Zotero.FeedReader", function () { var atomFeedURL = getTestDataUrl("feed.atom"); var mediaFeedURL = getTestDataUrl("feedMedia.xml"); - after(function* () { - yield clearFeeds(); + var win; + + before(async function() { + // Browser window is needed as parent window to load the feed reader scripts. + win = await loadBrowserWindow(); + }); + + after(async function() { + if (win) { + win.close(); + } + await clearFeeds(); }); describe('FeedReader()', function () { diff --git a/test/tests/feedTest.js b/test/tests/feedTest.js index 15e7937983..6766f47ea0 100644 --- a/test/tests/feedTest.js +++ b/test/tests/feedTest.js @@ -311,8 +311,11 @@ describe("Zotero.Feed", function() { var feed, scheduleNextFeedCheck; var feedUrl = getTestDataUrl("feed.rss"); var modifiedFeedUrl = getTestDataUrl("feedModified.rss"); + var win; - before(function() { + before(async function() { + // Browser window is needed as parent window to load the feed reader scripts. + win = await loadBrowserWindow(); scheduleNextFeedCheck = sinon.stub(Zotero.Feeds, 'scheduleNextFeedCheck').resolves(); }); @@ -328,6 +331,9 @@ describe("Zotero.Feed", function() { }); after(function() { + if (win) { + win.close(); + } scheduleNextFeedCheck.restore(); });