1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/feeds/FeedProcessor.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1797 @@ 1.4 +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +function LOG(str) { 1.10 + dump("*** " + str + "\n"); 1.11 +} 1.12 + 1.13 +const Ci = Components.interfaces; 1.14 +const Cc = Components.classes; 1.15 +const Cr = Components.results; 1.16 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); 1.17 + 1.18 +const FP_CONTRACTID = "@mozilla.org/feed-processor;1"; 1.19 +const FP_CLASSID = Components.ID("{26acb1f0-28fc-43bc-867a-a46aabc85dd4}"); 1.20 +const FP_CLASSNAME = "Feed Processor"; 1.21 +const FR_CONTRACTID = "@mozilla.org/feed-result;1"; 1.22 +const FR_CLASSID = Components.ID("{072a5c3d-30c6-4f07-b87f-9f63d51403f2}"); 1.23 +const FR_CLASSNAME = "Feed Result"; 1.24 +const FEED_CONTRACTID = "@mozilla.org/feed;1"; 1.25 +const FEED_CLASSID = Components.ID("{5d0cfa97-69dd-4e5e-ac84-f253162e8f9a}"); 1.26 +const FEED_CLASSNAME = "Feed"; 1.27 +const ENTRY_CONTRACTID = "@mozilla.org/feed-entry;1"; 1.28 +const ENTRY_CLASSID = Components.ID("{8e4444ff-8e99-4bdd-aa7f-fb3c1c77319f}"); 1.29 +const ENTRY_CLASSNAME = "Feed Entry"; 1.30 +const TEXTCONSTRUCT_CONTRACTID = "@mozilla.org/feed-textconstruct;1"; 1.31 +const TEXTCONSTRUCT_CLASSID = 1.32 + Components.ID("{b992ddcd-3899-4320-9909-924b3e72c922}"); 1.33 +const TEXTCONSTRUCT_CLASSNAME = "Feed Text Construct"; 1.34 +const GENERATOR_CONTRACTID = "@mozilla.org/feed-generator;1"; 1.35 +const GENERATOR_CLASSID = 1.36 + Components.ID("{414af362-9ad8-4296-898e-62247f25a20e}"); 1.37 +const GENERATOR_CLASSNAME = "Feed Generator"; 1.38 +const PERSON_CONTRACTID = "@mozilla.org/feed-person;1"; 1.39 +const PERSON_CLASSID = Components.ID("{95c963b7-20b2-11db-92f6-001422106990}"); 1.40 +const PERSON_CLASSNAME = "Feed Person"; 1.41 + 1.42 +const IO_CONTRACTID = "@mozilla.org/network/io-service;1" 1.43 +const BAG_CONTRACTID = "@mozilla.org/hash-property-bag;1" 1.44 +const ARRAY_CONTRACTID = "@mozilla.org/array;1"; 1.45 +const SAX_CONTRACTID = "@mozilla.org/saxparser/xmlreader;1"; 1.46 +const PARSERUTILS_CONTRACTID = "@mozilla.org/parserutils;1"; 1.47 + 1.48 + 1.49 +var gIoService = null; 1.50 + 1.51 +const XMLNS = "http://www.w3.org/XML/1998/namespace"; 1.52 +const RSS090NS = "http://my.netscape.com/rdf/simple/0.9/"; 1.53 + 1.54 +/***** Some general utils *****/ 1.55 +function strToURI(link, base) { 1.56 + var base = base || null; 1.57 + if (!gIoService) 1.58 + gIoService = Cc[IO_CONTRACTID].getService(Ci.nsIIOService); 1.59 + try { 1.60 + return gIoService.newURI(link, null, base); 1.61 + } 1.62 + catch(e) { 1.63 + return null; 1.64 + } 1.65 +} 1.66 + 1.67 +function isArray(a) { 1.68 + return isObject(a) && a.constructor == Array; 1.69 +} 1.70 + 1.71 +function isObject(a) { 1.72 + return (a && typeof a == "object") || isFunction(a); 1.73 +} 1.74 + 1.75 +function isFunction(a) { 1.76 + return typeof a == "function"; 1.77 +} 1.78 + 1.79 +function isIID(a, iid) { 1.80 + var rv = false; 1.81 + try { 1.82 + a.QueryInterface(iid); 1.83 + rv = true; 1.84 + } 1.85 + catch(e) { 1.86 + } 1.87 + return rv; 1.88 +} 1.89 + 1.90 +function isIArray(a) { 1.91 + return isIID(a, Ci.nsIArray); 1.92 +} 1.93 + 1.94 +function isIFeedContainer(a) { 1.95 + return isIID(a, Ci.nsIFeedContainer); 1.96 +} 1.97 + 1.98 +function stripTags(someHTML) { 1.99 + return someHTML.replace(/<[^>]+>/g,""); 1.100 +} 1.101 + 1.102 +/** 1.103 + * Searches through an array of links and returns a JS array 1.104 + * of matching property bags. 1.105 + */ 1.106 +const IANA_URI = "http://www.iana.org/assignments/relation/"; 1.107 +function findAtomLinks(rel, links) { 1.108 + var rvLinks = []; 1.109 + for (var i = 0; i < links.length; ++i) { 1.110 + var linkElement = links.queryElementAt(i, Ci.nsIPropertyBag2); 1.111 + // atom:link MUST have @href 1.112 + if (bagHasKey(linkElement, "href")) { 1.113 + var relAttribute = null; 1.114 + if (bagHasKey(linkElement, "rel")) 1.115 + relAttribute = linkElement.getPropertyAsAString("rel") 1.116 + if ((!relAttribute && rel == "alternate") || relAttribute == rel) { 1.117 + rvLinks.push(linkElement); 1.118 + continue; 1.119 + } 1.120 + // catch relations specified by IANA URI 1.121 + if (relAttribute == IANA_URI + rel) { 1.122 + rvLinks.push(linkElement); 1.123 + } 1.124 + } 1.125 + } 1.126 + return rvLinks; 1.127 +} 1.128 + 1.129 +function xmlEscape(s) { 1.130 + s = s.replace(/&/g, "&"); 1.131 + s = s.replace(/>/g, ">"); 1.132 + s = s.replace(/</g, "<"); 1.133 + s = s.replace(/"/g, """); 1.134 + s = s.replace(/'/g, "'"); 1.135 + return s; 1.136 +} 1.137 + 1.138 +function arrayContains(array, element) { 1.139 + for (var i = 0; i < array.length; ++i) { 1.140 + if (array[i] == element) { 1.141 + return true; 1.142 + } 1.143 + } 1.144 + return false; 1.145 +} 1.146 + 1.147 +// XXX add hasKey to nsIPropertyBag 1.148 +function bagHasKey(bag, key) { 1.149 + try { 1.150 + bag.getProperty(key); 1.151 + return true; 1.152 + } 1.153 + catch (e) { 1.154 + return false; 1.155 + } 1.156 +} 1.157 + 1.158 +function makePropGetter(key) { 1.159 + return function FeedPropGetter(bag) { 1.160 + try { 1.161 + return value = bag.getProperty(key); 1.162 + } 1.163 + catch(e) { 1.164 + } 1.165 + return null; 1.166 + } 1.167 +} 1.168 + 1.169 +const RDF_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; 1.170 +// namespace map 1.171 +var gNamespaces = { 1.172 + "http://webns.net/mvcb/":"admin", 1.173 + "http://backend.userland.com/rss":"", 1.174 + "http://blogs.law.harvard.edu/tech/rss":"", 1.175 + "http://www.w3.org/2005/Atom":"atom", 1.176 + "http://purl.org/atom/ns#":"atom03", 1.177 + "http://purl.org/rss/1.0/modules/content/":"content", 1.178 + "http://purl.org/dc/elements/1.1/":"dc", 1.179 + "http://purl.org/dc/terms/":"dcterms", 1.180 + "http://www.w3.org/1999/02/22-rdf-syntax-ns#":"rdf", 1.181 + "http://purl.org/rss/1.0/":"rss1", 1.182 + "http://my.netscape.com/rdf/simple/0.9/":"rss1", 1.183 + "http://wellformedweb.org/CommentAPI/":"wfw", 1.184 + "http://purl.org/rss/1.0/modules/wiki/":"wiki", 1.185 + "http://www.w3.org/XML/1998/namespace":"xml", 1.186 + "http://search.yahoo.com/mrss/":"media", 1.187 + "http://search.yahoo.com/mrss":"media" 1.188 +} 1.189 + 1.190 +// We allow a very small set of namespaces in XHTML content, 1.191 +// for attributes only 1.192 +var gAllowedXHTMLNamespaces = { 1.193 + "http://www.w3.org/XML/1998/namespace":"xml", 1.194 + // if someone ns qualifies XHTML, we have to prefix it to avoid an 1.195 + // attribute collision. 1.196 + "http://www.w3.org/1999/xhtml":"xhtml" 1.197 +} 1.198 + 1.199 +function FeedResult() {} 1.200 +FeedResult.prototype = { 1.201 + bozo: false, 1.202 + doc: null, 1.203 + version: null, 1.204 + headers: null, 1.205 + uri: null, 1.206 + stylesheet: null, 1.207 + 1.208 + registerExtensionPrefix: function FR_registerExtensionPrefix(ns, prefix) { 1.209 + throw Cr.NS_ERROR_NOT_IMPLEMENTED; 1.210 + }, 1.211 + 1.212 + // XPCOM stuff 1.213 + classID: FR_CLASSID, 1.214 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFeedResult]) 1.215 +} 1.216 + 1.217 +function Feed() { 1.218 + this.subtitle = null; 1.219 + this.title = null; 1.220 + this.items = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray); 1.221 + this.link = null; 1.222 + this.id = null; 1.223 + this.generator = null; 1.224 + this.authors = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray); 1.225 + this.contributors = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray); 1.226 + this.baseURI = null; 1.227 + this.enclosureCount = 0; 1.228 + this.type = Ci.nsIFeed.TYPE_FEED; 1.229 +} 1.230 + 1.231 +Feed.prototype = { 1.232 + searchLists: { 1.233 + title: ["title", "rss1:title", "atom03:title", "atom:title"], 1.234 + subtitle: ["description","dc:description","rss1:description", 1.235 + "atom03:tagline","atom:subtitle"], 1.236 + items: ["items","atom03_entries","entries"], 1.237 + id: ["atom:id","rdf:about"], 1.238 + generator: ["generator"], 1.239 + authors : ["authors"], 1.240 + contributors: ["contributors"], 1.241 + title: ["title","rss1:title", "atom03:title","atom:title"], 1.242 + link: [["link",strToURI],["rss1:link",strToURI]], 1.243 + categories: ["categories", "dc:subject"], 1.244 + rights: ["atom03:rights","atom:rights"], 1.245 + cloud: ["cloud"], 1.246 + image: ["image", "rss1:image", "atom:logo"], 1.247 + textInput: ["textInput", "rss1:textinput"], 1.248 + skipDays: ["skipDays"], 1.249 + skipHours: ["skipHours"], 1.250 + updated: ["pubDate", "lastBuildDate", "atom03:modified", "dc:date", 1.251 + "dcterms:modified", "atom:updated"] 1.252 + }, 1.253 + 1.254 + normalize: function Feed_normalize() { 1.255 + fieldsToObj(this, this.searchLists); 1.256 + if (this.skipDays) 1.257 + this.skipDays = this.skipDays.getProperty("days"); 1.258 + if (this.skipHours) 1.259 + this.skipHours = this.skipHours.getProperty("hours"); 1.260 + 1.261 + if (this.updated) 1.262 + this.updated = dateParse(this.updated); 1.263 + 1.264 + // Assign Atom link if needed 1.265 + if (bagHasKey(this.fields, "links")) 1.266 + this._atomLinksToURI(); 1.267 + 1.268 + this._calcEnclosureCountAndFeedType(); 1.269 + 1.270 + // Resolve relative image links 1.271 + if (this.image && bagHasKey(this.image, "url")) 1.272 + this._resolveImageLink(); 1.273 + 1.274 + this._resetBagMembersToRawText([this.searchLists.subtitle, 1.275 + this.searchLists.title]); 1.276 + }, 1.277 + 1.278 + _calcEnclosureCountAndFeedType: function Feed_calcEnclosureCountAndFeedType() { 1.279 + var entries_with_enclosures = 0; 1.280 + var audio_count = 0; 1.281 + var image_count = 0; 1.282 + var video_count = 0; 1.283 + var other_count = 0; 1.284 + 1.285 + for (var i = 0; i < this.items.length; ++i) { 1.286 + var entry = this.items.queryElementAt(i, Ci.nsIFeedEntry); 1.287 + entry.QueryInterface(Ci.nsIFeedContainer); 1.288 + 1.289 + if (entry.enclosures && entry.enclosures.length > 0) { 1.290 + ++entries_with_enclosures; 1.291 + 1.292 + for (var e = 0; e < entry.enclosures.length; ++e) { 1.293 + var enc = entry.enclosures.queryElementAt(e, Ci.nsIWritablePropertyBag2); 1.294 + if (enc.hasKey("type")) { 1.295 + var enctype = enc.get("type"); 1.296 + 1.297 + if (/^audio/.test(enctype)) { 1.298 + ++audio_count; 1.299 + } else if (/^image/.test(enctype)) { 1.300 + ++image_count; 1.301 + } else if (/^video/.test(enctype)) { 1.302 + ++video_count; 1.303 + } else { 1.304 + ++other_count; 1.305 + } 1.306 + } else { 1.307 + ++other_count; 1.308 + } 1.309 + } 1.310 + } 1.311 + } 1.312 + 1.313 + var feedtype = Ci.nsIFeed.TYPE_FEED; 1.314 + 1.315 + // For a feed to be marked as TYPE_VIDEO, TYPE_AUDIO and TYPE_IMAGE, 1.316 + // we enforce two things: 1.317 + // 1.318 + // 1. all entries must have at least one enclosure 1.319 + // 2. all enclosures must be video for TYPE_VIDEO, audio for TYPE_AUDIO or image 1.320 + // for TYPE_IMAGE 1.321 + // 1.322 + // Otherwise it's a TYPE_FEED. 1.323 + if (entries_with_enclosures == this.items.length && other_count == 0) { 1.324 + if (audio_count > 0 && !video_count && !image_count) { 1.325 + feedtype = Ci.nsIFeed.TYPE_AUDIO; 1.326 + 1.327 + } else if (image_count > 0 && !audio_count && !video_count) { 1.328 + feedtype = Ci.nsIFeed.TYPE_IMAGE; 1.329 + 1.330 + } else if (video_count > 0 && !audio_count && !image_count) { 1.331 + feedtype = Ci.nsIFeed.TYPE_VIDEO; 1.332 + } 1.333 + } 1.334 + 1.335 + this.type = feedtype; 1.336 + this.enclosureCount = other_count + video_count + audio_count + image_count; 1.337 + }, 1.338 + 1.339 + _atomLinksToURI: function Feed_linkToURI() { 1.340 + var links = this.fields.getPropertyAsInterface("links", Ci.nsIArray); 1.341 + var alternates = findAtomLinks("alternate", links); 1.342 + if (alternates.length > 0) { 1.343 + var href = alternates[0].getPropertyAsAString("href"); 1.344 + var base; 1.345 + if (bagHasKey(alternates[0], "xml:base")) 1.346 + base = alternates[0].getPropertyAsAString("xml:base"); 1.347 + this.link = this._resolveURI(href, base); 1.348 + } 1.349 + }, 1.350 + 1.351 + _resolveImageLink: function Feed_resolveImageLink() { 1.352 + var base; 1.353 + if (bagHasKey(this.image, "xml:base")) 1.354 + base = this.image.getPropertyAsAString("xml:base"); 1.355 + var url = this._resolveURI(this.image.getPropertyAsAString("url"), base); 1.356 + if (url) 1.357 + this.image.setPropertyAsAString("url", url.spec); 1.358 + }, 1.359 + 1.360 + _resolveURI: function Feed_resolveURI(linkSpec, baseSpec) { 1.361 + var uri = null; 1.362 + try { 1.363 + var base = baseSpec ? strToURI(baseSpec, this.baseURI) : this.baseURI; 1.364 + uri = strToURI(linkSpec, base); 1.365 + } 1.366 + catch(e) { 1.367 + LOG(e); 1.368 + } 1.369 + 1.370 + return uri; 1.371 + }, 1.372 + 1.373 + // reset the bag to raw contents, not text constructs 1.374 + _resetBagMembersToRawText: function Feed_resetBagMembers(fieldLists) { 1.375 + for (var i=0; i<fieldLists.length; i++) { 1.376 + for (var j=0; j<fieldLists[i].length; j++) { 1.377 + if (bagHasKey(this.fields, fieldLists[i][j])) { 1.378 + var textConstruct = this.fields.getProperty(fieldLists[i][j]); 1.379 + this.fields.setPropertyAsAString(fieldLists[i][j], 1.380 + textConstruct.text); 1.381 + } 1.382 + } 1.383 + } 1.384 + }, 1.385 + 1.386 + // XPCOM stuff 1.387 + classID: FEED_CLASSID, 1.388 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFeed, Ci.nsIFeedContainer]) 1.389 +} 1.390 + 1.391 +function Entry() { 1.392 + this.summary = null; 1.393 + this.content = null; 1.394 + this.title = null; 1.395 + this.fields = Cc["@mozilla.org/hash-property-bag;1"]. 1.396 + createInstance(Ci.nsIWritablePropertyBag2); 1.397 + this.link = null; 1.398 + this.id = null; 1.399 + this.baseURI = null; 1.400 + this.updated = null; 1.401 + this.published = null; 1.402 + this.authors = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray); 1.403 + this.contributors = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray); 1.404 +} 1.405 + 1.406 +Entry.prototype = { 1.407 + fields: null, 1.408 + enclosures: null, 1.409 + mediaContent: null, 1.410 + 1.411 + searchLists: { 1.412 + title: ["title", "rss1:title", "atom03:title", "atom:title"], 1.413 + link: [["link",strToURI],["rss1:link",strToURI]], 1.414 + id: [["guid", makePropGetter("guid")], "rdf:about", 1.415 + "atom03:id", "atom:id"], 1.416 + authors : ["authors"], 1.417 + contributors: ["contributors"], 1.418 + summary: ["description", "rss1:description", "dc:description", 1.419 + "atom03:summary", "atom:summary"], 1.420 + content: ["content:encoded","atom03:content","atom:content"], 1.421 + rights: ["atom03:rights","atom:rights"], 1.422 + published: ["pubDate", "atom03:issued", "dcterms:issued", "atom:published"], 1.423 + updated: ["pubDate", "atom03:modified", "dc:date", "dcterms:modified", 1.424 + "atom:updated"] 1.425 + }, 1.426 + 1.427 + normalize: function Entry_normalize() { 1.428 + fieldsToObj(this, this.searchLists); 1.429 + 1.430 + // Assign Atom link if needed 1.431 + if (bagHasKey(this.fields, "links")) 1.432 + this._atomLinksToURI(); 1.433 + 1.434 + // Populate enclosures array 1.435 + this._populateEnclosures(); 1.436 + 1.437 + // The link might be a guid w/ permalink=true 1.438 + if (!this.link && bagHasKey(this.fields, "guid")) { 1.439 + var guid = this.fields.getProperty("guid"); 1.440 + var isPermaLink = true; 1.441 + 1.442 + if (bagHasKey(guid, "isPermaLink")) 1.443 + isPermaLink = guid.getProperty("isPermaLink").toLowerCase() != "false"; 1.444 + 1.445 + if (guid && isPermaLink) 1.446 + this.link = strToURI(guid.getProperty("guid")); 1.447 + } 1.448 + 1.449 + if (this.updated) 1.450 + this.updated = dateParse(this.updated); 1.451 + if (this.published) 1.452 + this.published = dateParse(this.published); 1.453 + 1.454 + this._resetBagMembersToRawText([this.searchLists.content, 1.455 + this.searchLists.summary, 1.456 + this.searchLists.title]); 1.457 + }, 1.458 + 1.459 + _populateEnclosures: function Entry_populateEnclosures() { 1.460 + if (bagHasKey(this.fields, "links")) 1.461 + this._atomLinksToEnclosures(); 1.462 + 1.463 + // Add RSS2 enclosure to enclosures 1.464 + if (bagHasKey(this.fields, "enclosure")) 1.465 + this._enclosureToEnclosures(); 1.466 + 1.467 + // Add media:content to enclosures 1.468 + if (bagHasKey(this.fields, "mediacontent")) 1.469 + this._mediacontentToEnclosures(); 1.470 + 1.471 + // Add media:content in media:group to enclosures 1.472 + if (bagHasKey(this.fields, "mediagroup")) 1.473 + this._mediagroupToEnclosures(); 1.474 + }, 1.475 + 1.476 + __enclosure_map: null, 1.477 + 1.478 + _addToEnclosures: function Entry_addToEnclosures(new_enc) { 1.479 + // items we add to the enclosures array get displayed in the FeedWriter and 1.480 + // they must have non-empty urls. 1.481 + if (!bagHasKey(new_enc, "url") || new_enc.getPropertyAsAString("url") == "") 1.482 + return; 1.483 + 1.484 + if (this.__enclosure_map == null) 1.485 + this.__enclosure_map = {}; 1.486 + 1.487 + var previous_enc = this.__enclosure_map[new_enc.getPropertyAsAString("url")]; 1.488 + 1.489 + if (previous_enc != undefined) { 1.490 + previous_enc.QueryInterface(Ci.nsIWritablePropertyBag2); 1.491 + 1.492 + if (!bagHasKey(previous_enc, "type") && bagHasKey(new_enc, "type")) 1.493 + previous_enc.setPropertyAsAString("type", new_enc.getPropertyAsAString("type")); 1.494 + 1.495 + if (!bagHasKey(previous_enc, "length") && bagHasKey(new_enc, "length")) 1.496 + previous_enc.setPropertyAsAString("length", new_enc.getPropertyAsAString("length")); 1.497 + 1.498 + return; 1.499 + } 1.500 + 1.501 + if (this.enclosures == null) { 1.502 + this.enclosures = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray); 1.503 + this.enclosures.QueryInterface(Ci.nsIMutableArray); 1.504 + } 1.505 + 1.506 + this.enclosures.appendElement(new_enc, false); 1.507 + this.__enclosure_map[new_enc.getPropertyAsAString("url")] = new_enc; 1.508 + }, 1.509 + 1.510 + _atomLinksToEnclosures: function Entry_linkToEnclosure() { 1.511 + var links = this.fields.getPropertyAsInterface("links", Ci.nsIArray); 1.512 + var enc_links = findAtomLinks("enclosure", links); 1.513 + if (enc_links.length == 0) 1.514 + return; 1.515 + 1.516 + for (var i = 0; i < enc_links.length; ++i) { 1.517 + var link = enc_links[i]; 1.518 + 1.519 + // an enclosure must have an href 1.520 + if (!(link.getProperty("href"))) 1.521 + return; 1.522 + 1.523 + var enc = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2); 1.524 + 1.525 + // copy Atom bits over to equivalent enclosure bits 1.526 + enc.setPropertyAsAString("url", link.getPropertyAsAString("href")); 1.527 + if (bagHasKey(link, "type")) 1.528 + enc.setPropertyAsAString("type", link.getPropertyAsAString("type")); 1.529 + if (bagHasKey(link, "length")) 1.530 + enc.setPropertyAsAString("length", link.getPropertyAsAString("length")); 1.531 + 1.532 + this._addToEnclosures(enc); 1.533 + } 1.534 + }, 1.535 + 1.536 + _enclosureToEnclosures: function Entry_enclosureToEnclosures() { 1.537 + var enc = this.fields.getPropertyAsInterface("enclosure", Ci.nsIPropertyBag2); 1.538 + 1.539 + if (!(enc.getProperty("url"))) 1.540 + return; 1.541 + 1.542 + this._addToEnclosures(enc); 1.543 + }, 1.544 + 1.545 + _mediacontentToEnclosures: function Entry_mediacontentToEnclosures() { 1.546 + var mediacontent = this.fields.getPropertyAsInterface("mediacontent", Ci.nsIArray); 1.547 + 1.548 + for (var i = 0; i < mediacontent.length; ++i) { 1.549 + var contentElement = mediacontent.queryElementAt(i, Ci.nsIWritablePropertyBag2); 1.550 + 1.551 + // media:content don't require url, but if it's not there, we should 1.552 + // skip it. 1.553 + if (!bagHasKey(contentElement, "url")) 1.554 + continue; 1.555 + 1.556 + var enc = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2); 1.557 + 1.558 + // copy media:content bits over to equivalent enclosure bits 1.559 + enc.setPropertyAsAString("url", contentElement.getPropertyAsAString("url")); 1.560 + if (bagHasKey(contentElement, "type")) { 1.561 + enc.setPropertyAsAString("type", contentElement.getPropertyAsAString("type")); 1.562 + } 1.563 + if (bagHasKey(contentElement, "fileSize")) { 1.564 + enc.setPropertyAsAString("length", contentElement.getPropertyAsAString("fileSize")); 1.565 + } 1.566 + 1.567 + this._addToEnclosures(enc); 1.568 + } 1.569 + }, 1.570 + 1.571 + _mediagroupToEnclosures: function Entry_mediagroupToEnclosures() { 1.572 + var group = this.fields.getPropertyAsInterface("mediagroup", Ci.nsIPropertyBag2); 1.573 + 1.574 + var content = group.getPropertyAsInterface("mediacontent", Ci.nsIArray); 1.575 + for (var i = 0; i < content.length; ++i) { 1.576 + var contentElement = content.queryElementAt(i, Ci.nsIWritablePropertyBag2); 1.577 + // media:content don't require url, but if it's not there, we should 1.578 + // skip it. 1.579 + if (!bagHasKey(contentElement, "url")) 1.580 + continue; 1.581 + 1.582 + var enc = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2); 1.583 + 1.584 + // copy media:content bits over to equivalent enclosure bits 1.585 + enc.setPropertyAsAString("url", contentElement.getPropertyAsAString("url")); 1.586 + if (bagHasKey(contentElement, "type")) { 1.587 + enc.setPropertyAsAString("type", contentElement.getPropertyAsAString("type")); 1.588 + } 1.589 + if (bagHasKey(contentElement, "fileSize")) { 1.590 + enc.setPropertyAsAString("length", contentElement.getPropertyAsAString("fileSize")); 1.591 + } 1.592 + 1.593 + this._addToEnclosures(enc); 1.594 + } 1.595 + }, 1.596 + 1.597 + // XPCOM stuff 1.598 + classID: ENTRY_CLASSID, 1.599 + QueryInterface: XPCOMUtils.generateQI( 1.600 + [Ci.nsIFeedEntry, Ci.nsIFeedContainer] 1.601 + ) 1.602 +} 1.603 + 1.604 +Entry.prototype._atomLinksToURI = Feed.prototype._atomLinksToURI; 1.605 +Entry.prototype._resolveURI = Feed.prototype._resolveURI; 1.606 +Entry.prototype._resetBagMembersToRawText = 1.607 + Feed.prototype._resetBagMembersToRawText; 1.608 + 1.609 +// TextConstruct represents and element that could contain (X)HTML 1.610 +function TextConstruct() { 1.611 + this.lang = null; 1.612 + this.base = null; 1.613 + this.type = "text"; 1.614 + this.text = null; 1.615 + this.parserUtils = Cc[PARSERUTILS_CONTRACTID].getService(Ci.nsIParserUtils); 1.616 +} 1.617 + 1.618 +TextConstruct.prototype = { 1.619 + plainText: function TC_plainText() { 1.620 + if (this.type != "text") { 1.621 + return this.parserUtils.convertToPlainText(stripTags(this.text), 1.622 + Ci.nsIDocumentEncoder.OutputSelectionOnly | 1.623 + Ci.nsIDocumentEncoder.OutputAbsoluteLinks, 1.624 + 0); 1.625 + } 1.626 + return this.text; 1.627 + }, 1.628 + 1.629 + createDocumentFragment: function TC_createDocumentFragment(element) { 1.630 + if (this.type == "text") { 1.631 + var doc = element.ownerDocument; 1.632 + var docFragment = doc.createDocumentFragment(); 1.633 + var node = doc.createTextNode(this.text); 1.634 + docFragment.appendChild(node); 1.635 + return docFragment; 1.636 + } 1.637 + var isXML; 1.638 + if (this.type == "xhtml") 1.639 + isXML = true 1.640 + else if (this.type == "html") 1.641 + isXML = false; 1.642 + else 1.643 + return null; 1.644 + 1.645 + return this.parserUtils.parseFragment(this.text, 0, isXML, 1.646 + this.base, element); 1.647 + }, 1.648 + 1.649 + // XPCOM stuff 1.650 + classID: TEXTCONSTRUCT_CLASSID, 1.651 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFeedTextConstruct]) 1.652 +} 1.653 + 1.654 +// Generator represents the software that produced the feed 1.655 +function Generator() { 1.656 + this.lang = null; 1.657 + this.agent = null; 1.658 + this.version = null; 1.659 + this.uri = null; 1.660 + 1.661 + // nsIFeedElementBase 1.662 + this._attributes = null; 1.663 + this.baseURI = null; 1.664 +} 1.665 + 1.666 +Generator.prototype = { 1.667 + 1.668 + get attributes() { 1.669 + return this._attributes; 1.670 + }, 1.671 + 1.672 + set attributes(value) { 1.673 + this._attributes = value; 1.674 + this.version = this._attributes.getValueFromName("","version"); 1.675 + var uriAttribute = this._attributes.getValueFromName("","uri") || 1.676 + this._attributes.getValueFromName("","url"); 1.677 + this.uri = strToURI(uriAttribute, this.baseURI); 1.678 + 1.679 + // RSS1 1.680 + uriAttribute = this._attributes.getValueFromName(RDF_NS,"resource"); 1.681 + if (uriAttribute) { 1.682 + this.agent = uriAttribute; 1.683 + this.uri = strToURI(uriAttribute, this.baseURI); 1.684 + } 1.685 + }, 1.686 + 1.687 + // XPCOM stuff 1.688 + classID: GENERATOR_CLASSID, 1.689 + QueryInterface: XPCOMUtils.generateQI( 1.690 + [Ci.nsIFeedGenerator, Ci.nsIFeedElementBase] 1.691 + ) 1.692 +} 1.693 + 1.694 +function Person() { 1.695 + this.name = null; 1.696 + this.uri = null; 1.697 + this.email = null; 1.698 + 1.699 + // nsIFeedElementBase 1.700 + this.attributes = null; 1.701 + this.baseURI = null; 1.702 +} 1.703 + 1.704 +Person.prototype = { 1.705 + // XPCOM stuff 1.706 + classID: PERSON_CLASSID, 1.707 + QueryInterface: XPCOMUtils.generateQI( 1.708 + [Ci.nsIFeedPerson, Ci.nsIFeedElementBase] 1.709 + ) 1.710 +} 1.711 + 1.712 +/** 1.713 + * Map a list of fields into properties on a container. 1.714 + * 1.715 + * @param container An nsIFeedContainer 1.716 + * @param fields A list of fields to search for. List members can 1.717 + * be a list, in which case the second member is 1.718 + * transformation function (like parseInt). 1.719 + */ 1.720 +function fieldsToObj(container, fields) { 1.721 + var props,prop,field,searchList; 1.722 + for (var key in fields) { 1.723 + searchList = fields[key]; 1.724 + for (var i=0; i < searchList.length; ++i) { 1.725 + props = searchList[i]; 1.726 + prop = null; 1.727 + field = isArray(props) ? props[0] : props; 1.728 + try { 1.729 + prop = container.fields.getProperty(field); 1.730 + } 1.731 + catch(e) { 1.732 + } 1.733 + if (prop) { 1.734 + prop = isArray(props) ? props[1](prop) : prop; 1.735 + container[key] = prop; 1.736 + } 1.737 + } 1.738 + } 1.739 +} 1.740 + 1.741 +/** 1.742 + * Lower cases an element's localName property 1.743 + * @param element A DOM element. 1.744 + * 1.745 + * @returns The lower case localName property of the specified element 1.746 + */ 1.747 +function LC(element) { 1.748 + return element.localName.toLowerCase(); 1.749 +} 1.750 + 1.751 +// TODO move these post-processor functions 1.752 +// create a generator element 1.753 +function atomGenerator(s, generator) { 1.754 + generator.QueryInterface(Ci.nsIFeedGenerator); 1.755 + generator.agent = s.trim(); 1.756 + return generator; 1.757 +} 1.758 + 1.759 +// post-process atom:logo to create an RSS2-like structure 1.760 +function atomLogo(s, logo) { 1.761 + logo.setPropertyAsAString("url", s.trim()); 1.762 +} 1.763 + 1.764 +// post-process an RSS category, map it to the Atom fields. 1.765 +function rssCatTerm(s, cat) { 1.766 + // add slash handling? 1.767 + cat.setPropertyAsAString("term", s.trim()); 1.768 + return cat; 1.769 +} 1.770 + 1.771 +// post-process a GUID 1.772 +function rssGuid(s, guid) { 1.773 + guid.setPropertyAsAString("guid", s.trim()); 1.774 + return guid; 1.775 +} 1.776 + 1.777 +// post-process an RSS author element 1.778 +// 1.779 +// It can contain a field like this: 1.780 +// 1.781 +// <author>lawyer@boyer.net (Lawyer Boyer)</author> 1.782 +// 1.783 +// or, delightfully, a field like this: 1.784 +// 1.785 +// <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator> 1.786 +// 1.787 +// We want to split this up and assign it to corresponding Atom 1.788 +// fields. 1.789 +// 1.790 +function rssAuthor(s,author) { 1.791 + author.QueryInterface(Ci.nsIFeedPerson); 1.792 + // check for RSS2 string format 1.793 + var chars = s.trim(); 1.794 + var matches = chars.match(/(.*)\((.*)\)/); 1.795 + var emailCheck = 1.796 + /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/; 1.797 + if (matches) { 1.798 + var match1 = matches[1].trim(); 1.799 + var match2 = matches[2].trim(); 1.800 + if (match2.indexOf("mailto:") == 0) 1.801 + match2 = match2.substring(7); 1.802 + if (emailCheck.test(match1)) { 1.803 + author.email = match1; 1.804 + author.name = match2; 1.805 + } 1.806 + else if (emailCheck.test(match2)) { 1.807 + author.email = match2; 1.808 + author.name = match1; 1.809 + } 1.810 + else { 1.811 + // put it back together 1.812 + author.name = match1 + " (" + match2 + ")"; 1.813 + } 1.814 + } 1.815 + else { 1.816 + author.name = chars; 1.817 + if (chars.indexOf('@')) 1.818 + author.email = chars; 1.819 + } 1.820 + return author; 1.821 +} 1.822 + 1.823 +// 1.824 +// skipHours and skipDays map to arrays, so we need to change the 1.825 +// string to an nsISupports in order to stick it in there. 1.826 +// 1.827 +function rssArrayElement(s) { 1.828 + var str = Cc["@mozilla.org/supports-string;1"]. 1.829 + createInstance(Ci.nsISupportsString); 1.830 + str.data = s; 1.831 + str.QueryInterface(Ci.nsISupportsString); 1.832 + return str; 1.833 +} 1.834 + 1.835 +/** 1.836 + * Tries parsing a string through the JavaScript Date object. 1.837 + * @param aDateString 1.838 + * A string that is supposedly an RFC822 or RFC3339 date. 1.839 + * @return A Date.toUTCString, or null if the string can't be parsed. 1.840 + */ 1.841 +function dateParse(aDateString) { 1.842 + let dateString = aDateString.trim(); 1.843 + // Without bug 682781 fixed, JS won't parse an RFC822 date with a Z for the 1.844 + // timezone, so convert to -00:00 which works for any date format. 1.845 + dateString = dateString.replace(/z$/i, "-00:00"); 1.846 + let date = new Date(dateString); 1.847 + if (!isNaN(date)) { 1.848 + return date.toUTCString(); 1.849 + } 1.850 + return null; 1.851 +} 1.852 + 1.853 +const XHTML_NS = "http://www.w3.org/1999/xhtml"; 1.854 + 1.855 +// The XHTMLHandler handles inline XHTML found in things like atom:summary 1.856 +function XHTMLHandler(processor, isAtom) { 1.857 + this._buf = ""; 1.858 + this._processor = processor; 1.859 + this._depth = 0; 1.860 + this._isAtom = isAtom; 1.861 + // a stack of lists tracking in-scope namespaces 1.862 + this._inScopeNS = []; 1.863 +} 1.864 + 1.865 +// The fidelity can be improved here, to allow handling of stuff like 1.866 +// SVG and MathML. XXX 1.867 +XHTMLHandler.prototype = { 1.868 + 1.869 + // look back up at the declared namespaces 1.870 + // we always use the same prefixes for our safe stuff 1.871 + _isInScope: function XH__isInScope(ns) { 1.872 + for (var i in this._inScopeNS) { 1.873 + for (var uri in this._inScopeNS[i]) { 1.874 + if (this._inScopeNS[i][uri] == ns) 1.875 + return true; 1.876 + } 1.877 + } 1.878 + return false; 1.879 + }, 1.880 + 1.881 + startDocument: function XH_startDocument() { 1.882 + }, 1.883 + endDocument: function XH_endDocument() { 1.884 + }, 1.885 + startElement: function XH_startElement(uri, localName, qName, attributes) { 1.886 + ++this._depth; 1.887 + this._inScopeNS.push([]); 1.888 + 1.889 + // RFC4287 requires XHTML to be wrapped in a div that is *not* part of 1.890 + // the content. This prevents people from screwing up namespaces, but 1.891 + // we need to skip it here. 1.892 + if (this._isAtom && this._depth == 1 && localName == "div") 1.893 + return; 1.894 + 1.895 + // If it's an XHTML element, record it. Otherwise, it's ignored. 1.896 + if (uri == XHTML_NS) { 1.897 + this._buf += "<" + localName; 1.898 + var uri; 1.899 + for (var i=0; i < attributes.length; ++i) { 1.900 + uri = attributes.getURI(i); 1.901 + // XHTML attributes aren't in a namespace 1.902 + if (uri == "") { 1.903 + this._buf += (" " + attributes.getLocalName(i) + "='" + 1.904 + xmlEscape(attributes.getValue(i)) + "'"); 1.905 + } else { 1.906 + // write a small set of allowed attribute namespaces 1.907 + var prefix = gAllowedXHTMLNamespaces[uri]; 1.908 + if (prefix != null) { 1.909 + // The attribute value we'll attempt to write 1.910 + var attributeValue = xmlEscape(attributes.getValue(i)); 1.911 + 1.912 + // it's an allowed attribute NS. 1.913 + // write the attribute 1.914 + this._buf += (" " + prefix + ":" + 1.915 + attributes.getLocalName(i) + 1.916 + "='" + attributeValue + "'"); 1.917 + 1.918 + // write an xmlns declaration if necessary 1.919 + if (prefix != "xml" && !this._isInScope(uri)) { 1.920 + this._inScopeNS[this._inScopeNS.length - 1].push(uri); 1.921 + this._buf += " xmlns:" + prefix + "='" + uri + "'"; 1.922 + } 1.923 + } 1.924 + } 1.925 + } 1.926 + this._buf += ">"; 1.927 + } 1.928 + }, 1.929 + endElement: function XH_endElement(uri, localName, qName) { 1.930 + --this._depth; 1.931 + this._inScopeNS.pop(); 1.932 + 1.933 + // We need to skip outer divs in Atom. See comment in startElement. 1.934 + if (this._isAtom && this._depth == 0 && localName == "div") 1.935 + return; 1.936 + 1.937 + // When we peek too far, go back to the main processor 1.938 + if (this._depth < 0) { 1.939 + this._processor.returnFromXHTMLHandler(this._buf.trim(), 1.940 + uri, localName, qName); 1.941 + return; 1.942 + } 1.943 + // If it's an XHTML element, record it. Otherwise, it's ignored. 1.944 + if (uri == XHTML_NS) { 1.945 + this._buf += "</" + localName + ">"; 1.946 + } 1.947 + }, 1.948 + characters: function XH_characters(data) { 1.949 + this._buf += xmlEscape(data); 1.950 + }, 1.951 + startPrefixMapping: function XH_startPrefixMapping(prefix, uri) { 1.952 + }, 1.953 + endPrefixMapping: function FP_endPrefixMapping(prefix) { 1.954 + }, 1.955 + processingInstruction: function XH_processingInstruction() { 1.956 + }, 1.957 +} 1.958 + 1.959 +/** 1.960 + * The ExtensionHandler deals with elements we haven't explicitly 1.961 + * added to our transition table in the FeedProcessor. 1.962 + */ 1.963 +function ExtensionHandler(processor) { 1.964 + this._buf = ""; 1.965 + this._depth = 0; 1.966 + this._hasChildElements = false; 1.967 + 1.968 + // The FeedProcessor 1.969 + this._processor = processor; 1.970 + 1.971 + // Fields of the outermost extension element. 1.972 + this._localName = null; 1.973 + this._uri = null; 1.974 + this._qName = null; 1.975 + this._attrs = null; 1.976 +} 1.977 + 1.978 +ExtensionHandler.prototype = { 1.979 + startDocument: function EH_startDocument() { 1.980 + }, 1.981 + endDocument: function EH_endDocument() { 1.982 + }, 1.983 + startElement: function EH_startElement(uri, localName, qName, attrs) { 1.984 + ++this._depth; 1.985 + var prefix = gNamespaces[uri] ? gNamespaces[uri] + ":" : ""; 1.986 + var key = prefix + localName; 1.987 + 1.988 + if (this._depth == 1) { 1.989 + this._uri = uri; 1.990 + this._localName = localName; 1.991 + this._qName = qName; 1.992 + this._attrs = attrs; 1.993 + } 1.994 + 1.995 + // if we descend into another element, we won't send text 1.996 + this._hasChildElements = (this._depth > 1); 1.997 + 1.998 + }, 1.999 + endElement: function EH_endElement(uri, localName, qName) { 1.1000 + --this._depth; 1.1001 + if (this._depth == 0) { 1.1002 + var text = this._hasChildElements ? null : this._buf.trim(); 1.1003 + this._processor.returnFromExtHandler(this._uri, this._localName, 1.1004 + text, this._attrs); 1.1005 + } 1.1006 + }, 1.1007 + characters: function EH_characters(data) { 1.1008 + if (!this._hasChildElements) 1.1009 + this._buf += data; 1.1010 + }, 1.1011 + startPrefixMapping: function EH_startPrefixMapping() { 1.1012 + }, 1.1013 + endPrefixMapping: function EH_endPrefixMapping() { 1.1014 + }, 1.1015 + processingInstruction: function EH_processingInstruction() { 1.1016 + }, 1.1017 +}; 1.1018 + 1.1019 + 1.1020 +/** 1.1021 + * ElementInfo is a simple container object that describes 1.1022 + * some characteristics of a feed element. For example, it 1.1023 + * says whether an element can be expected to appear more 1.1024 + * than once inside a given entry or feed. 1.1025 + */ 1.1026 +function ElementInfo(fieldName, containerClass, closeFunc, isArray) { 1.1027 + this.fieldName = fieldName; 1.1028 + this.containerClass = containerClass; 1.1029 + this.closeFunc = closeFunc; 1.1030 + this.isArray = isArray; 1.1031 + this.isWrapper = false; 1.1032 +} 1.1033 + 1.1034 +/** 1.1035 + * FeedElementInfo represents a feed element, usually the root. 1.1036 + */ 1.1037 +function FeedElementInfo(fieldName, feedVersion) { 1.1038 + this.isWrapper = false; 1.1039 + this.fieldName = fieldName; 1.1040 + this.feedVersion = feedVersion; 1.1041 +} 1.1042 + 1.1043 +/** 1.1044 + * Some feed formats include vestigial wrapper elements that we don't 1.1045 + * want to include in our object model, but we do need to keep track 1.1046 + * of during parsing. 1.1047 + */ 1.1048 +function WrapperElementInfo(fieldName) { 1.1049 + this.isWrapper = true; 1.1050 + this.fieldName = fieldName; 1.1051 +} 1.1052 + 1.1053 +/***** The Processor *****/ 1.1054 +function FeedProcessor() { 1.1055 + this._reader = Cc[SAX_CONTRACTID].createInstance(Ci.nsISAXXMLReader); 1.1056 + this._buf = ""; 1.1057 + this._feed = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2); 1.1058 + this._handlerStack = []; 1.1059 + this._xmlBaseStack = []; // sparse array keyed to nesting depth 1.1060 + this._depth = 0; 1.1061 + this._state = "START"; 1.1062 + this._result = null; 1.1063 + this._extensionHandler = null; 1.1064 + this._xhtmlHandler = null; 1.1065 + this._haveSentResult = false; 1.1066 + 1.1067 + // The nsIFeedResultListener waiting for the parse results 1.1068 + this.listener = null; 1.1069 + 1.1070 + // These elements can contain (X)HTML or plain text. 1.1071 + // We keep a table here that contains their default treatment 1.1072 + this._textConstructs = {"atom:title":"text", 1.1073 + "atom:summary":"text", 1.1074 + "atom:rights":"text", 1.1075 + "atom:content":"text", 1.1076 + "atom:subtitle":"text", 1.1077 + "description":"html", 1.1078 + "rss1:description":"html", 1.1079 + "dc:description":"html", 1.1080 + "content:encoded":"html", 1.1081 + "title":"text", 1.1082 + "rss1:title":"text", 1.1083 + "atom03:title":"text", 1.1084 + "atom03:tagline":"text", 1.1085 + "atom03:summary":"text", 1.1086 + "atom03:content":"text"}; 1.1087 + this._stack = []; 1.1088 + 1.1089 + this._trans = { 1.1090 + "START": { 1.1091 + //If we hit a root RSS element, treat as RSS2. 1.1092 + "rss": new FeedElementInfo("RSS2", "rss2"), 1.1093 + 1.1094 + // If we hit an RDF element, if could be RSS1, but we can't 1.1095 + // verify that until we hit a rss1:channel element. 1.1096 + "rdf:RDF": new WrapperElementInfo("RDF"), 1.1097 + 1.1098 + // If we hit a Atom 1.0 element, treat as Atom 1.0. 1.1099 + "atom:feed": new FeedElementInfo("Atom", "atom"), 1.1100 + 1.1101 + // Treat as Atom 0.3 1.1102 + "atom03:feed": new FeedElementInfo("Atom03", "atom03"), 1.1103 + }, 1.1104 + 1.1105 + /********* RSS2 **********/ 1.1106 + "IN_RSS2": { 1.1107 + "channel": new WrapperElementInfo("channel") 1.1108 + }, 1.1109 + 1.1110 + "IN_CHANNEL": { 1.1111 + "item": new ElementInfo("items", Cc[ENTRY_CONTRACTID], null, true), 1.1112 + "managingEditor": new ElementInfo("authors", Cc[PERSON_CONTRACTID], 1.1113 + rssAuthor, true), 1.1114 + "dc:creator": new ElementInfo("authors", Cc[PERSON_CONTRACTID], 1.1115 + rssAuthor, true), 1.1116 + "dc:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID], 1.1117 + rssAuthor, true), 1.1118 + "dc:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID], 1.1119 + rssAuthor, true), 1.1120 + "category": new ElementInfo("categories", null, rssCatTerm, true), 1.1121 + "cloud": new ElementInfo("cloud", null, null, false), 1.1122 + "image": new ElementInfo("image", null, null, false), 1.1123 + "textInput": new ElementInfo("textInput", null, null, false), 1.1124 + "skipDays": new ElementInfo("skipDays", null, null, false), 1.1125 + "skipHours": new ElementInfo("skipHours", null, null, false), 1.1126 + "generator": new ElementInfo("generator", Cc[GENERATOR_CONTRACTID], 1.1127 + atomGenerator, false), 1.1128 + }, 1.1129 + 1.1130 + "IN_ITEMS": { 1.1131 + "author": new ElementInfo("authors", Cc[PERSON_CONTRACTID], 1.1132 + rssAuthor, true), 1.1133 + "dc:creator": new ElementInfo("authors", Cc[PERSON_CONTRACTID], 1.1134 + rssAuthor, true), 1.1135 + "dc:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID], 1.1136 + rssAuthor, true), 1.1137 + "dc:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID], 1.1138 + rssAuthor, true), 1.1139 + "category": new ElementInfo("categories", null, rssCatTerm, true), 1.1140 + "enclosure": new ElementInfo("enclosure", null, null, false), 1.1141 + "media:content": new ElementInfo("mediacontent", null, null, true), 1.1142 + "media:group": new ElementInfo("mediagroup", null, null, false), 1.1143 + "guid": new ElementInfo("guid", null, rssGuid, false) 1.1144 + }, 1.1145 + 1.1146 + "IN_SKIPDAYS": { 1.1147 + "day": new ElementInfo("days", null, rssArrayElement, true) 1.1148 + }, 1.1149 + 1.1150 + "IN_SKIPHOURS":{ 1.1151 + "hour": new ElementInfo("hours", null, rssArrayElement, true) 1.1152 + }, 1.1153 + 1.1154 + "IN_MEDIAGROUP": { 1.1155 + "media:content": new ElementInfo("mediacontent", null, null, true) 1.1156 + }, 1.1157 + 1.1158 + /********* RSS1 **********/ 1.1159 + "IN_RDF": { 1.1160 + // If we hit a rss1:channel, we can verify that we have RSS1 1.1161 + "rss1:channel": new FeedElementInfo("rdf_channel", "rss1"), 1.1162 + "rss1:image": new ElementInfo("image", null, null, false), 1.1163 + "rss1:textinput": new ElementInfo("textInput", null, null, false), 1.1164 + "rss1:item": new ElementInfo("items", Cc[ENTRY_CONTRACTID], null, true), 1.1165 + }, 1.1166 + 1.1167 + "IN_RDF_CHANNEL": { 1.1168 + "admin:generatorAgent": new ElementInfo("generator", 1.1169 + Cc[GENERATOR_CONTRACTID], 1.1170 + null, false), 1.1171 + "dc:creator": new ElementInfo("authors", Cc[PERSON_CONTRACTID], 1.1172 + rssAuthor, true), 1.1173 + "dc:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID], 1.1174 + rssAuthor, true), 1.1175 + "dc:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID], 1.1176 + rssAuthor, true), 1.1177 + }, 1.1178 + 1.1179 + /********* ATOM 1.0 **********/ 1.1180 + "IN_ATOM": { 1.1181 + "atom:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID], 1.1182 + null, true), 1.1183 + "atom:generator": new ElementInfo("generator", Cc[GENERATOR_CONTRACTID], 1.1184 + atomGenerator, false), 1.1185 + "atom:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID], 1.1186 + null, true), 1.1187 + "atom:link": new ElementInfo("links", null, null, true), 1.1188 + "atom:logo": new ElementInfo("atom:logo", null, atomLogo, false), 1.1189 + "atom:entry": new ElementInfo("entries", Cc[ENTRY_CONTRACTID], 1.1190 + null, true) 1.1191 + }, 1.1192 + 1.1193 + "IN_ENTRIES": { 1.1194 + "atom:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID], 1.1195 + null, true), 1.1196 + "atom:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID], 1.1197 + null, true), 1.1198 + "atom:link": new ElementInfo("links", null, null, true), 1.1199 + }, 1.1200 + 1.1201 + /********* ATOM 0.3 **********/ 1.1202 + "IN_ATOM03": { 1.1203 + "atom03:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID], 1.1204 + null, true), 1.1205 + "atom03:contributor": new ElementInfo("contributors", 1.1206 + Cc[PERSON_CONTRACTID], 1.1207 + null, true), 1.1208 + "atom03:link": new ElementInfo("links", null, null, true), 1.1209 + "atom03:entry": new ElementInfo("atom03_entries", Cc[ENTRY_CONTRACTID], 1.1210 + null, true), 1.1211 + "atom03:generator": new ElementInfo("generator", Cc[GENERATOR_CONTRACTID], 1.1212 + atomGenerator, false), 1.1213 + }, 1.1214 + 1.1215 + "IN_ATOM03_ENTRIES": { 1.1216 + "atom03:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID], 1.1217 + null, true), 1.1218 + "atom03:contributor": new ElementInfo("contributors", 1.1219 + Cc[PERSON_CONTRACTID], 1.1220 + null, true), 1.1221 + "atom03:link": new ElementInfo("links", null, null, true), 1.1222 + "atom03:entry": new ElementInfo("atom03_entries", Cc[ENTRY_CONTRACTID], 1.1223 + null, true) 1.1224 + } 1.1225 + } 1.1226 +} 1.1227 + 1.1228 +// See startElement for a long description of how feeds are processed. 1.1229 +FeedProcessor.prototype = { 1.1230 + 1.1231 + // Set ourselves as the SAX handler, and set the base URI 1.1232 + _init: function FP_init(uri) { 1.1233 + this._reader.contentHandler = this; 1.1234 + this._reader.errorHandler = this; 1.1235 + this._result = Cc[FR_CONTRACTID].createInstance(Ci.nsIFeedResult); 1.1236 + if (uri) { 1.1237 + this._result.uri = uri; 1.1238 + this._reader.baseURI = uri; 1.1239 + this._xmlBaseStack[0] = uri; 1.1240 + } 1.1241 + }, 1.1242 + 1.1243 + // This function is called once we figure out what type of feed 1.1244 + // we're dealing with. Some feed types require digging a bit further 1.1245 + // than the root. 1.1246 + _docVerified: function FP_docVerified(version) { 1.1247 + this._result.doc = Cc[FEED_CONTRACTID].createInstance(Ci.nsIFeed); 1.1248 + this._result.doc.baseURI = 1.1249 + this._xmlBaseStack[this._xmlBaseStack.length - 1]; 1.1250 + this._result.doc.fields = this._feed; 1.1251 + this._result.version = version; 1.1252 + }, 1.1253 + 1.1254 + // When we're done with the feed, let the listener know what 1.1255 + // happened. 1.1256 + _sendResult: function FP_sendResult() { 1.1257 + this._haveSentResult = true; 1.1258 + try { 1.1259 + // Can be null when a non-feed is fed to us 1.1260 + if (this._result.doc) 1.1261 + this._result.doc.normalize(); 1.1262 + } 1.1263 + catch (e) { 1.1264 + LOG("FIXME: " + e); 1.1265 + } 1.1266 + 1.1267 + try { 1.1268 + if (this.listener != null) 1.1269 + this.listener.handleResult(this._result); 1.1270 + } 1.1271 + finally { 1.1272 + this._result = null; 1.1273 + } 1.1274 + }, 1.1275 + 1.1276 + // Parsing functions 1.1277 + parseFromStream: function FP_parseFromStream(stream, uri) { 1.1278 + this._init(uri); 1.1279 + this._reader.parseFromStream(stream, null, stream.available(), 1.1280 + "application/xml"); 1.1281 + this._reader = null; 1.1282 + }, 1.1283 + 1.1284 + parseFromString: function FP_parseFromString(inputString, uri) { 1.1285 + this._init(uri); 1.1286 + this._reader.parseFromString(inputString, "application/xml"); 1.1287 + this._reader = null; 1.1288 + }, 1.1289 + 1.1290 + parseAsync: function FP_parseAsync(requestObserver, uri) { 1.1291 + this._init(uri); 1.1292 + this._reader.parseAsync(requestObserver); 1.1293 + }, 1.1294 + 1.1295 + // nsIStreamListener 1.1296 + 1.1297 + // The XMLReader will throw sensible exceptions if these get called 1.1298 + // out of order. 1.1299 + onStartRequest: function FP_onStartRequest(request, context) { 1.1300 + // this will throw if the request is not a channel, but so will nsParser. 1.1301 + var channel = request.QueryInterface(Ci.nsIChannel); 1.1302 + channel.contentType = "application/vnd.mozilla.maybe.feed"; 1.1303 + this._reader.onStartRequest(request, context); 1.1304 + }, 1.1305 + 1.1306 + onStopRequest: function FP_onStopRequest(request, context, statusCode) { 1.1307 + try { 1.1308 + this._reader.onStopRequest(request, context, statusCode); 1.1309 + } 1.1310 + finally { 1.1311 + this._reader = null; 1.1312 + } 1.1313 + }, 1.1314 + 1.1315 + onDataAvailable: 1.1316 + function FP_onDataAvailable(request, context, inputStream, offset, count) { 1.1317 + this._reader.onDataAvailable(request, context, inputStream, offset, count); 1.1318 + }, 1.1319 + 1.1320 + // nsISAXErrorHandler 1.1321 + 1.1322 + // We only care about fatal errors. When this happens, we may have 1.1323 + // parsed through the feed metadata and some number of entries. The 1.1324 + // listener can still show some of that data if it wants, and we'll 1.1325 + // set the bozo bit to indicate we were unable to parse all the way 1.1326 + // through. 1.1327 + fatalError: function FP_reportError() { 1.1328 + this._result.bozo = true; 1.1329 + //XXX need to QI to FeedProgressListener 1.1330 + if (!this._haveSentResult) 1.1331 + this._sendResult(); 1.1332 + }, 1.1333 + 1.1334 + // nsISAXContentHandler 1.1335 + 1.1336 + startDocument: function FP_startDocument() { 1.1337 + //LOG("----------"); 1.1338 + }, 1.1339 + 1.1340 + endDocument: function FP_endDocument() { 1.1341 + if (!this._haveSentResult) 1.1342 + this._sendResult(); 1.1343 + }, 1.1344 + 1.1345 + // The transitions defined above identify elements that contain more 1.1346 + // than just text. For example RSS items contain many fields, and so 1.1347 + // do Atom authors. The only commonly used elements that contain 1.1348 + // mixed content are Atom Text Constructs of type="xhtml", which we 1.1349 + // delegate to another handler for cleaning. That leaves a couple 1.1350 + // different types of elements to deal with: those that should occur 1.1351 + // only once, such as title elements, and those that can occur 1.1352 + // multiple times, such as the RSS category element and the Atom 1.1353 + // link element. Most of the RSS1/DC elements can occur multiple 1.1354 + // times in theory, but in practice, the only ones that do have 1.1355 + // analogues in Atom. 1.1356 + // 1.1357 + // Some elements are also groups of attributes or sub-elements, 1.1358 + // while others are simple text fields. For the most part, we don't 1.1359 + // have to pay explicit attention to the simple text elements, 1.1360 + // unless we want to post-process the resulting string to transform 1.1361 + // it into some richer object like a Date or URI. 1.1362 + // 1.1363 + // Elements that have more sophisticated content models still end up 1.1364 + // being dictionaries, whether they are based on attributes like RSS 1.1365 + // cloud, sub-elements like Atom author, or even items and 1.1366 + // entries. These elements are treated as "containers". It's 1.1367 + // theoretically possible for a container to have an attribute with 1.1368 + // the same universal name as a sub-element, but none of the feed 1.1369 + // formats allow this by default, and I don't of any extension that 1.1370 + // works this way. 1.1371 + // 1.1372 + startElement: function FP_startElement(uri, localName, qName, attributes) { 1.1373 + this._buf = ""; 1.1374 + ++this._depth; 1.1375 + var elementInfo; 1.1376 + 1.1377 + //LOG("<" + localName + ">"); 1.1378 + 1.1379 + // Check for xml:base 1.1380 + var base = attributes.getValueFromName(XMLNS, "base"); 1.1381 + if (base) { 1.1382 + this._xmlBaseStack[this._depth] = 1.1383 + strToURI(base, this._xmlBaseStack[this._xmlBaseStack.length - 1]); 1.1384 + } 1.1385 + 1.1386 + // To identify the element we're dealing with, we look up the 1.1387 + // namespace URI in our gNamespaces dictionary, which will give us 1.1388 + // a "canonical" prefix for a namespace URI. For example, this 1.1389 + // allows Dublin Core "creator" elements to be consistently mapped 1.1390 + // to "dc:creator", for easy field access by consumer code. This 1.1391 + // strategy also happens to shorten up our state table. 1.1392 + var key = this._prefixForNS(uri) + localName; 1.1393 + 1.1394 + // Check to see if we need to hand this off to our XHTML handler. 1.1395 + // The elements we're dealing with will look like this: 1.1396 + // 1.1397 + // <title type="xhtml"> 1.1398 + // <div xmlns="http://www.w3.org/1999/xhtml"> 1.1399 + // A title with <b>bold</b> and <i>italics</i>. 1.1400 + // </div> 1.1401 + // </title> 1.1402 + // 1.1403 + // When it returns in returnFromXHTMLHandler, the handler should 1.1404 + // give us back a string like this: 1.1405 + // 1.1406 + // "A title with <b>bold</b> and <i>italics</i>." 1.1407 + // 1.1408 + // The Atom spec explicitly says the div is not part of the content, 1.1409 + // and explicitly allows whitespace collapsing. 1.1410 + // 1.1411 + if ((this._result.version == "atom" || this._result.version == "atom03") && 1.1412 + this._textConstructs[key] != null) { 1.1413 + var type = attributes.getValueFromName("","type"); 1.1414 + if (type != null && type.indexOf("xhtml") >= 0) { 1.1415 + this._xhtmlHandler = 1.1416 + new XHTMLHandler(this, (this._result.version == "atom")); 1.1417 + this._reader.contentHandler = this._xhtmlHandler; 1.1418 + return; 1.1419 + } 1.1420 + } 1.1421 + 1.1422 + // Check our current state, and see if that state has a defined 1.1423 + // transition. For example, this._trans["atom:entry"]["atom:author"] 1.1424 + // will have one, and it tells us to add an item to our authors array. 1.1425 + if (this._trans[this._state] && this._trans[this._state][key]) { 1.1426 + elementInfo = this._trans[this._state][key]; 1.1427 + } 1.1428 + else { 1.1429 + // If we don't have a transition, hand off to extension handler 1.1430 + this._extensionHandler = new ExtensionHandler(this); 1.1431 + this._reader.contentHandler = this._extensionHandler; 1.1432 + this._extensionHandler.startElement(uri, localName, qName, attributes); 1.1433 + return; 1.1434 + } 1.1435 + 1.1436 + // This distinguishes wrappers like 'channel' from elements 1.1437 + // we'd actually like to do something with (which will test true). 1.1438 + this._handlerStack[this._depth] = elementInfo; 1.1439 + if (elementInfo.isWrapper) { 1.1440 + this._state = "IN_" + elementInfo.fieldName.toUpperCase(); 1.1441 + this._stack.push([this._feed, this._state]); 1.1442 + } 1.1443 + else if (elementInfo.feedVersion) { 1.1444 + this._state = "IN_" + elementInfo.fieldName.toUpperCase(); 1.1445 + 1.1446 + // Check for the older RSS2 variants 1.1447 + if (elementInfo.feedVersion == "rss2") 1.1448 + elementInfo.feedVersion = this._findRSSVersion(attributes); 1.1449 + else if (uri == RSS090NS) 1.1450 + elementInfo.feedVersion = "rss090"; 1.1451 + 1.1452 + this._docVerified(elementInfo.feedVersion); 1.1453 + this._stack.push([this._feed, this._state]); 1.1454 + this._mapAttributes(this._feed, attributes); 1.1455 + } 1.1456 + else { 1.1457 + this._state = this._processComplexElement(elementInfo, attributes); 1.1458 + } 1.1459 + }, 1.1460 + 1.1461 + // In the endElement handler, we decrement the stack and look 1.1462 + // for cleanup/transition functions to execute. The second part 1.1463 + // of the state transition works as above in startElement, but 1.1464 + // the state we're looking for is prefixed with an underscore 1.1465 + // to distinguish endElement events from startElement events. 1.1466 + endElement: function FP_endElement(uri, localName, qName) { 1.1467 + var elementInfo = this._handlerStack[this._depth]; 1.1468 + //LOG("</" + localName + ">"); 1.1469 + if (elementInfo && !elementInfo.isWrapper) 1.1470 + this._closeComplexElement(elementInfo); 1.1471 + 1.1472 + // cut down xml:base context 1.1473 + if (this._xmlBaseStack.length == this._depth + 1) 1.1474 + this._xmlBaseStack = this._xmlBaseStack.slice(0, this._depth); 1.1475 + 1.1476 + // our new state is whatever is at the top of the stack now 1.1477 + if (this._stack.length > 0) 1.1478 + this._state = this._stack[this._stack.length - 1][1]; 1.1479 + this._handlerStack = this._handlerStack.slice(0, this._depth); 1.1480 + --this._depth; 1.1481 + }, 1.1482 + 1.1483 + // Buffer up character data. The buffer is cleared with every 1.1484 + // opening element. 1.1485 + characters: function FP_characters(data) { 1.1486 + this._buf += data; 1.1487 + }, 1.1488 + // TODO: It would be nice to check new prefixes here, and if they 1.1489 + // don't conflict with the ones we've defined, throw them in a 1.1490 + // dictionary to check. 1.1491 + startPrefixMapping: function FP_startPrefixMapping(prefix, uri) { 1.1492 + }, 1.1493 + 1.1494 + endPrefixMapping: function FP_endPrefixMapping(prefix) { 1.1495 + }, 1.1496 + 1.1497 + processingInstruction: function FP_processingInstruction(target, data) { 1.1498 + if (target == "xml-stylesheet") { 1.1499 + var hrefAttribute = data.match(/href=[\"\'](.*?)[\"\']/); 1.1500 + if (hrefAttribute && hrefAttribute.length == 2) 1.1501 + this._result.stylesheet = strToURI(hrefAttribute[1], this._result.uri); 1.1502 + } 1.1503 + }, 1.1504 + 1.1505 + // end of nsISAXContentHandler 1.1506 + 1.1507 + // Handle our more complicated elements--those that contain 1.1508 + // attributes and child elements. 1.1509 + _processComplexElement: 1.1510 + function FP__processComplexElement(elementInfo, attributes) { 1.1511 + var obj, key, prefix; 1.1512 + 1.1513 + // If the container is an entry/item, it'll need to have its 1.1514 + // more esoteric properties put in the 'fields' property bag. 1.1515 + if (elementInfo.containerClass == Cc[ENTRY_CONTRACTID]) { 1.1516 + obj = elementInfo.containerClass.createInstance(Ci.nsIFeedEntry); 1.1517 + obj.baseURI = this._xmlBaseStack[this._xmlBaseStack.length - 1]; 1.1518 + this._mapAttributes(obj.fields, attributes); 1.1519 + } 1.1520 + else if (elementInfo.containerClass) { 1.1521 + obj = elementInfo.containerClass.createInstance(Ci.nsIFeedElementBase); 1.1522 + obj.baseURI = this._xmlBaseStack[this._xmlBaseStack.length - 1]; 1.1523 + obj.attributes = attributes; // just set the SAX attributes 1.1524 + } 1.1525 + else { 1.1526 + obj = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2); 1.1527 + this._mapAttributes(obj, attributes); 1.1528 + } 1.1529 + 1.1530 + // We should have a container/propertyBag that's had its 1.1531 + // attributes processed. Now we need to attach it to its 1.1532 + // container. 1.1533 + var newProp; 1.1534 + 1.1535 + // First we'll see what's on top of the stack. 1.1536 + var container = this._stack[this._stack.length - 1][0]; 1.1537 + 1.1538 + // Check to see if it has the property 1.1539 + var prop; 1.1540 + try { 1.1541 + prop = container.getProperty(elementInfo.fieldName); 1.1542 + } 1.1543 + catch(e) { 1.1544 + } 1.1545 + 1.1546 + if (elementInfo.isArray) { 1.1547 + if (!prop) { 1.1548 + container.setPropertyAsInterface(elementInfo.fieldName, 1.1549 + Cc[ARRAY_CONTRACTID]. 1.1550 + createInstance(Ci.nsIMutableArray)); 1.1551 + } 1.1552 + 1.1553 + newProp = container.getProperty(elementInfo.fieldName); 1.1554 + // XXX This QI should not be necessary, but XPConnect seems to fly 1.1555 + // off the handle in the browser, and loses track of the interface 1.1556 + // on large files. Bug 335638. 1.1557 + newProp.QueryInterface(Ci.nsIMutableArray); 1.1558 + newProp.appendElement(obj,false); 1.1559 + 1.1560 + // If new object is an nsIFeedContainer, we want to deal with 1.1561 + // its member nsIPropertyBag instead. 1.1562 + if (isIFeedContainer(obj)) 1.1563 + newProp = obj.fields; 1.1564 + 1.1565 + } 1.1566 + else { 1.1567 + // If it doesn't, set it. 1.1568 + if (!prop) { 1.1569 + container.setPropertyAsInterface(elementInfo.fieldName,obj); 1.1570 + } 1.1571 + newProp = container.getProperty(elementInfo.fieldName); 1.1572 + } 1.1573 + 1.1574 + // make our new state name, and push the property onto the stack 1.1575 + var newState = "IN_" + elementInfo.fieldName.toUpperCase(); 1.1576 + this._stack.push([newProp, newState, obj]); 1.1577 + return newState; 1.1578 + }, 1.1579 + 1.1580 + // Sometimes we need reconcile the element content with the object 1.1581 + // model for a given feed. We use helper functions to do the 1.1582 + // munging, but we need to identify array types here, so the munging 1.1583 + // happens only to the last element of an array. 1.1584 + _closeComplexElement: function FP__closeComplexElement(elementInfo) { 1.1585 + var stateTuple = this._stack.pop(); 1.1586 + var container = stateTuple[0]; 1.1587 + var containerParent = stateTuple[2]; 1.1588 + var element = null; 1.1589 + var isArray = isIArray(container); 1.1590 + 1.1591 + // If it's an array and we have to post-process, 1.1592 + // grab the last element 1.1593 + if (isArray) 1.1594 + element = container.queryElementAt(container.length - 1, Ci.nsISupports); 1.1595 + else 1.1596 + element = container; 1.1597 + 1.1598 + // Run the post-processing function if there is one. 1.1599 + if (elementInfo.closeFunc) 1.1600 + element = elementInfo.closeFunc(this._buf, element); 1.1601 + 1.1602 + // If an nsIFeedContainer was on top of the stack, 1.1603 + // we need to normalize it 1.1604 + if (elementInfo.containerClass == Cc[ENTRY_CONTRACTID]) 1.1605 + containerParent.normalize(); 1.1606 + 1.1607 + // If it's an array, re-set the last element 1.1608 + if (isArray) 1.1609 + container.replaceElementAt(element, container.length - 1, false); 1.1610 + }, 1.1611 + 1.1612 + _prefixForNS: function FP_prefixForNS(uri) { 1.1613 + if (!uri) 1.1614 + return ""; 1.1615 + var prefix = gNamespaces[uri]; 1.1616 + if (prefix) 1.1617 + return prefix + ":"; 1.1618 + if (uri.toLowerCase().indexOf("http://backend.userland.com") == 0) 1.1619 + return ""; 1.1620 + else 1.1621 + return null; 1.1622 + }, 1.1623 + 1.1624 + _mapAttributes: function FP__mapAttributes(bag, attributes) { 1.1625 + // Cycle through the attributes, and set our properties using the 1.1626 + // prefix:localNames we find in our namespace dictionary. 1.1627 + for (var i = 0; i < attributes.length; ++i) { 1.1628 + var key = this._prefixForNS(attributes.getURI(i)) + attributes.getLocalName(i); 1.1629 + var val = attributes.getValue(i); 1.1630 + bag.setPropertyAsAString(key, val); 1.1631 + } 1.1632 + }, 1.1633 + 1.1634 + // Only for RSS2esque formats 1.1635 + _findRSSVersion: function FP__findRSSVersion(attributes) { 1.1636 + var versionAttr = attributes.getValueFromName("", "version").trim(); 1.1637 + var versions = { "0.91":"rss091", 1.1638 + "0.92":"rss092", 1.1639 + "0.93":"rss093", 1.1640 + "0.94":"rss094" } 1.1641 + if (versions[versionAttr]) 1.1642 + return versions[versionAttr]; 1.1643 + if (versionAttr.substr(0,2) != "2.") 1.1644 + return "rssUnknown"; 1.1645 + return "rss2"; 1.1646 + }, 1.1647 + 1.1648 + // unknown element values are returned here. See startElement above 1.1649 + // for how this works. 1.1650 + returnFromExtHandler: 1.1651 + function FP_returnExt(uri, localName, chars, attributes) { 1.1652 + --this._depth; 1.1653 + 1.1654 + // take control of the SAX events 1.1655 + this._reader.contentHandler = this; 1.1656 + if (localName == null && chars == null) 1.1657 + return; 1.1658 + 1.1659 + // we don't take random elements inside rdf:RDF 1.1660 + if (this._state == "IN_RDF") 1.1661 + return; 1.1662 + 1.1663 + // Grab the top of the stack 1.1664 + var top = this._stack[this._stack.length - 1]; 1.1665 + if (!top) 1.1666 + return; 1.1667 + 1.1668 + var container = top[0]; 1.1669 + // Grab the last element if it's an array 1.1670 + if (isIArray(container)) { 1.1671 + var contract = this._handlerStack[this._depth].containerClass; 1.1672 + // check if it's something specific, but not an entry 1.1673 + if (contract && contract != Cc[ENTRY_CONTRACTID]) { 1.1674 + var el = container.queryElementAt(container.length - 1, 1.1675 + Ci.nsIFeedElementBase); 1.1676 + // XXX there must be a way to flatten these interfaces 1.1677 + if (contract == Cc[PERSON_CONTRACTID]) 1.1678 + el.QueryInterface(Ci.nsIFeedPerson); 1.1679 + else 1.1680 + return; // don't know about this interface 1.1681 + 1.1682 + var propName = localName; 1.1683 + var prefix = gNamespaces[uri]; 1.1684 + 1.1685 + // synonyms 1.1686 + if ((uri == "" || 1.1687 + prefix && 1.1688 + ((prefix.indexOf("atom") > -1) || 1.1689 + (prefix.indexOf("rss") > -1))) && 1.1690 + (propName == "url" || propName == "href")) 1.1691 + propName = "uri"; 1.1692 + 1.1693 + try { 1.1694 + if (el[propName] !== "undefined") { 1.1695 + var propValue = chars; 1.1696 + // convert URI-bearing values to an nsIURI 1.1697 + if (propName == "uri") { 1.1698 + var base = this._xmlBaseStack[this._xmlBaseStack.length - 1]; 1.1699 + propValue = strToURI(chars, base); 1.1700 + } 1.1701 + el[propName] = propValue; 1.1702 + } 1.1703 + } 1.1704 + catch(e) { 1.1705 + // ignore XPConnect errors 1.1706 + } 1.1707 + // the rest of the function deals with entry- and feed-level stuff 1.1708 + return; 1.1709 + } 1.1710 + else { 1.1711 + container = container.queryElementAt(container.length - 1, 1.1712 + Ci.nsIWritablePropertyBag2); 1.1713 + } 1.1714 + } 1.1715 + 1.1716 + // Make the buffer our new property 1.1717 + var propName = this._prefixForNS(uri) + localName; 1.1718 + 1.1719 + // But, it could be something containing HTML. If so, 1.1720 + // we need to know about that. 1.1721 + if (this._textConstructs[propName] != null && 1.1722 + this._handlerStack[this._depth].containerClass !== null) { 1.1723 + var newProp = Cc[TEXTCONSTRUCT_CONTRACTID]. 1.1724 + createInstance(Ci.nsIFeedTextConstruct); 1.1725 + newProp.text = chars; 1.1726 + // Look up the default type in our table 1.1727 + var type = this._textConstructs[propName]; 1.1728 + var typeAttribute = attributes.getValueFromName("","type"); 1.1729 + if (this._result.version == "atom" && typeAttribute != null) { 1.1730 + type = typeAttribute; 1.1731 + } 1.1732 + else if (this._result.version == "atom03" && typeAttribute != null) { 1.1733 + if (typeAttribute.toLowerCase().indexOf("xhtml") >= 0) { 1.1734 + type = "xhtml"; 1.1735 + } 1.1736 + else if (typeAttribute.toLowerCase().indexOf("html") >= 0) { 1.1737 + type = "html"; 1.1738 + } 1.1739 + else if (typeAttribute.toLowerCase().indexOf("text") >= 0) { 1.1740 + type = "text"; 1.1741 + } 1.1742 + } 1.1743 + 1.1744 + // If it's rss feed-level description, it's not supposed to have html 1.1745 + if (this._result.version.indexOf("rss") >= 0 && 1.1746 + this._handlerStack[this._depth].containerClass != ENTRY_CONTRACTID) { 1.1747 + type = "text"; 1.1748 + } 1.1749 + newProp.type = type; 1.1750 + newProp.base = this._xmlBaseStack[this._xmlBaseStack.length - 1]; 1.1751 + container.setPropertyAsInterface(propName, newProp); 1.1752 + } 1.1753 + else { 1.1754 + container.setPropertyAsAString(propName, chars); 1.1755 + } 1.1756 + }, 1.1757 + 1.1758 + // Sometimes, we'll hand off SAX handling duties to an XHTMLHandler 1.1759 + // (see above) that will scrape out non-XHTML stuff, normalize 1.1760 + // namespaces, and remove the wrapper div from Atom 1.0. When the 1.1761 + // XHTMLHandler is done, it'll callback here. 1.1762 + returnFromXHTMLHandler: 1.1763 + function FP_returnFromXHTMLHandler(chars, uri, localName, qName) { 1.1764 + // retake control of the SAX content events 1.1765 + this._reader.contentHandler = this; 1.1766 + 1.1767 + // Grab the top of the stack 1.1768 + var top = this._stack[this._stack.length - 1]; 1.1769 + if (!top) 1.1770 + return; 1.1771 + var container = top[0]; 1.1772 + 1.1773 + // Assign the property 1.1774 + var newProp = newProp = Cc[TEXTCONSTRUCT_CONTRACTID]. 1.1775 + createInstance(Ci.nsIFeedTextConstruct); 1.1776 + newProp.text = chars; 1.1777 + newProp.type = "xhtml"; 1.1778 + newProp.base = this._xmlBaseStack[this._xmlBaseStack.length - 1]; 1.1779 + container.setPropertyAsInterface(this._prefixForNS(uri) + localName, 1.1780 + newProp); 1.1781 + 1.1782 + // XHTML will cause us to peek too far. The XHTML handler will 1.1783 + // send us an end element to call. RFC4287-valid feeds allow a 1.1784 + // more graceful way to handle this. Unfortunately, we can't count 1.1785 + // on compliance at this point. 1.1786 + this.endElement(uri, localName, qName); 1.1787 + }, 1.1788 + 1.1789 + // XPCOM stuff 1.1790 + classID: FP_CLASSID, 1.1791 + QueryInterface: XPCOMUtils.generateQI( 1.1792 + [Ci.nsIFeedProcessor, Ci.nsISAXContentHandler, Ci.nsISAXErrorHandler, 1.1793 + Ci.nsIStreamListener, Ci.nsIRequestObserver] 1.1794 + ) 1.1795 +} 1.1796 + 1.1797 +var components = [FeedProcessor, FeedResult, Feed, Entry, 1.1798 + TextConstruct, Generator, Person]; 1.1799 + 1.1800 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);