summaryrefslogtreecommitdiff
path: root/mailnews/extensions/newsblog/content/FeedUtils.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'mailnews/extensions/newsblog/content/FeedUtils.jsm')
-rw-r--r--mailnews/extensions/newsblog/content/FeedUtils.jsm1608
1 files changed, 1608 insertions, 0 deletions
diff --git a/mailnews/extensions/newsblog/content/FeedUtils.jsm b/mailnews/extensions/newsblog/content/FeedUtils.jsm
new file mode 100644
index 0000000000..6d5e64dd22
--- /dev/null
+++ b/mailnews/extensions/newsblog/content/FeedUtils.jsm
@@ -0,0 +1,1608 @@
+/* -*- Mode: JavaScript; tab-width: 2; 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/. */
+
+this.EXPORTED_SYMBOLS = ["Feed", "FeedItem", "FeedParser", "FeedUtils"];
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource:///modules/gloda/log4moz.js");
+Cu.import("resource:///modules/mailServices.js");
+Cu.import("resource:///modules/MailUtils.js");
+Cu.import("resource:///modules/jsmime.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+Services.scriptloader.loadSubScript("chrome://messenger-newsblog/content/Feed.js");
+Services.scriptloader.loadSubScript("chrome://messenger-newsblog/content/FeedItem.js");
+Services.scriptloader.loadSubScript("chrome://messenger-newsblog/content/feed-parser.js");
+
+var FeedUtils = {
+ MOZ_PARSERERROR_NS: "http://www.mozilla.org/newlayout/xml/parsererror.xml",
+
+ RDF_SYNTAX_NS: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+ RDF_SYNTAX_TYPE: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
+ get RDF_TYPE() { return this.rdf.GetResource(this.RDF_SYNTAX_TYPE) },
+
+ RSS_090_NS: "http://my.netscape.com/rdf/simple/0.9/",
+
+ RSS_NS: "http://purl.org/rss/1.0/",
+ get RSS_CHANNEL() { return this.rdf.GetResource(this.RSS_NS + "channel") },
+ get RSS_TITLE() { return this.rdf.GetResource(this.RSS_NS + "title") },
+ get RSS_DESCRIPTION() { return this.rdf.GetResource(this.RSS_NS + "description") },
+ get RSS_ITEMS() { return this.rdf.GetResource(this.RSS_NS + "items") },
+ get RSS_ITEM() { return this.rdf.GetResource(this.RSS_NS + "item") },
+ get RSS_LINK() { return this.rdf.GetResource(this.RSS_NS + "link") },
+
+ RSS_CONTENT_NS: "http://purl.org/rss/1.0/modules/content/",
+ get RSS_CONTENT_ENCODED() {
+ return this.rdf.GetResource(this.RSS_CONTENT_NS + "encoded");
+ },
+
+ DC_NS: "http://purl.org/dc/elements/1.1/",
+ get DC_CREATOR() { return this.rdf.GetResource(this.DC_NS + "creator") },
+ get DC_SUBJECT() { return this.rdf.GetResource(this.DC_NS + "subject") },
+ get DC_DATE() { return this.rdf.GetResource(this.DC_NS + "date") },
+ get DC_TITLE() { return this.rdf.GetResource(this.DC_NS + "title") },
+ get DC_LASTMODIFIED() { return this.rdf.GetResource(this.DC_NS + "lastModified") },
+ get DC_IDENTIFIER() { return this.rdf.GetResource(this.DC_NS + "identifier") },
+
+ MRSS_NS: "http://search.yahoo.com/mrss/",
+ FEEDBURNER_NS: "http://rssnamespace.org/feedburner/ext/1.0",
+ ITUNES_NS: "http://www.itunes.com/dtds/podcast-1.0.dtd",
+
+ FZ_NS: "urn:forumzilla:",
+ FZ_ITEM_NS: "urn:feeditem:",
+ get FZ_ROOT() { return this.rdf.GetResource(this.FZ_NS + "root") },
+ get FZ_FEEDS() { return this.rdf.GetResource(this.FZ_NS + "feeds") },
+ get FZ_FEED() { return this.rdf.GetResource(this.FZ_NS + "feed") },
+ get FZ_QUICKMODE() { return this.rdf.GetResource(this.FZ_NS + "quickMode") },
+ get FZ_DESTFOLDER() { return this.rdf.GetResource(this.FZ_NS + "destFolder") },
+ get FZ_STORED() { return this.rdf.GetResource(this.FZ_NS + "stored") },
+ get FZ_VALID() { return this.rdf.GetResource(this.FZ_NS + "valid") },
+ get FZ_OPTIONS() { return this.rdf.GetResource(this.FZ_NS + "options"); },
+ get FZ_LAST_SEEN_TIMESTAMP() {
+ return this.rdf.GetResource(this.FZ_NS + "last-seen-timestamp");
+ },
+
+ get RDF_LITERAL_TRUE() { return this.rdf.GetLiteral("true") },
+ get RDF_LITERAL_FALSE() { return this.rdf.GetLiteral("false") },
+
+ // Atom constants
+ ATOM_03_NS: "http://purl.org/atom/ns#",
+ ATOM_IETF_NS: "http://www.w3.org/2005/Atom",
+ ATOM_THREAD_NS: "http://purl.org/syndication/thread/1.0",
+
+ // Accept content mimetype preferences for feeds.
+ REQUEST_ACCEPT: "application/atom+xml," +
+ "application/rss+xml;q=0.9," +
+ "application/rdf+xml;q=0.8," +
+ "application/xml;q=0.7,text/xml;q=0.7," +
+ "*/*;q=0.1",
+ // Timeout for nonresponse to request, 30 seconds.
+ REQUEST_TIMEOUT: 30 * 1000,
+
+ // The approximate amount of time, specified in milliseconds, to leave an
+ // item in the RDF cache after the item has dissappeared from feeds.
+ // The delay is currently one day.
+ INVALID_ITEM_PURGE_DELAY: 24 * 60 * 60 * 1000,
+
+ kBiffMinutesDefault: 100,
+ kNewsBlogSuccess: 0,
+ // Usually means there was an error trying to parse the feed.
+ kNewsBlogInvalidFeed: 1,
+ // Generic networking failure when trying to download the feed.
+ kNewsBlogRequestFailure: 2,
+ kNewsBlogFeedIsBusy: 3,
+ // For 304 Not Modified; There are no new articles for this feed.
+ kNewsBlogNoNewItems: 4,
+ kNewsBlogCancel: 5,
+ kNewsBlogFileError: 6,
+ // Invalid certificate, for overridable user exception errors.
+ kNewsBlogBadCertError: 7,
+ // For 401 Unauthorized or 403 Forbidden.
+ kNewsBlogNoAuthError: 8,
+
+ CANCEL_REQUESTED: false,
+ AUTOTAG: "~AUTOTAG",
+
+/**
+ * Get all rss account servers rootFolders.
+ *
+ * @return array of nsIMsgIncomingServer (empty array if none).
+ */
+ getAllRssServerRootFolders: function() {
+ let rssRootFolders = [];
+ let allServers = MailServices.accounts.allServers;
+ for (let i = 0; i < allServers.length; i++)
+ {
+ let server = allServers.queryElementAt(i, Ci.nsIMsgIncomingServer);
+ if (server && server.type == "rss")
+ rssRootFolders.push(server.rootFolder);
+ }
+
+ // By default, Tb sorts by hostname, ie Feeds, Feeds-1, and not by alpha
+ // prettyName. Do the same as a stock install to match folderpane order.
+ rssRootFolders.sort(function(a, b) { return a.hostname > b.hostname });
+
+ return rssRootFolders;
+ },
+
+/**
+ * Create rss account.
+ *
+ * @param string [aName] - optional account name to override default.
+ * @return nsIMsgAccount.
+ */
+ createRssAccount: function(aName) {
+ let userName = "nobody";
+ let hostName = "Feeds";
+ let hostNamePref = hostName;
+ let server;
+ let serverType = "rss";
+ let defaultName = FeedUtils.strings.GetStringFromName("feeds-accountname");
+ let i = 2;
+ while (MailServices.accounts.findRealServer(userName, hostName, serverType, 0))
+ // If "Feeds" exists, try "Feeds-2", then "Feeds-3", etc.
+ hostName = hostNamePref + "-" + i++;
+
+ server = MailServices.accounts.createIncomingServer(userName, hostName, serverType);
+ server.biffMinutes = FeedUtils.kBiffMinutesDefault;
+ server.prettyName = aName ? aName : defaultName;
+ server.valid = true;
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = server;
+
+ // Ensure the Trash folder db (.msf) is created otherwise folder/message
+ // deletes will throw until restart creates it.
+ server.msgStore.discoverSubFolders(server.rootMsgFolder, false);
+
+ // Create "Local Folders" if none exist yet as it's guaranteed that
+ // those exist when any account exists.
+ let localFolders;
+ try {
+ localFolders = MailServices.accounts.localFoldersServer;
+ }
+ catch (ex) {}
+
+ if (!localFolders)
+ MailServices.accounts.createLocalMailAccount();
+
+ // Save new accounts in case of a crash.
+ try {
+ MailServices.accounts.saveAccountInfo();
+ }
+ catch (ex) {
+ this.log.error("FeedUtils.createRssAccount: error on saveAccountInfo - " + ex);
+ }
+
+ this.log.debug("FeedUtils.createRssAccount: " +
+ account.incomingServer.rootFolder.prettyName);
+
+ return account;
+ },
+
+/**
+ * Helper routine that checks our subscriptions list array and returns
+ * true if the url is already in our list. This is used to prevent the
+ * user from subscribing to the same feed multiple times for the same server.
+ *
+ * @param string aUrl - the url.
+ * @param nsIMsgIncomingServer aServer - account server.
+ * @return boolean - true if exists else false.
+ */
+ feedAlreadyExists: function(aUrl, aServer) {
+ let ds = this.getSubscriptionsDS(aServer);
+ let feeds = this.getSubscriptionsList(ds);
+ let resource = this.rdf.GetResource(aUrl);
+ if (feeds.IndexOf(resource) == -1)
+ return false;
+
+ let folder = ds.GetTarget(resource, FeedUtils.FZ_DESTFOLDER, true)
+ .QueryInterface(Ci.nsIRDFResource).ValueUTF8;
+ this.log.info("FeedUtils.feedAlreadyExists: feed url " + aUrl +
+ " subscribed in folder url " + decodeURI(folder));
+
+ return true;
+ },
+
+/**
+ * Download a feed url on biff or get new messages.
+ *
+ * @param nsIMsgFolder aFolder - folder
+ * @param nsIUrlListener aUrlListener - feed url
+ * @param bool aIsBiff - true if biff, false if manual get
+ * @param nsIDOMWindow aMsgWindow - window
+ */
+ downloadFeed: function(aFolder, aUrlListener, aIsBiff, aMsgWindow) {
+ if (Services.io.offline)
+ return;
+
+ // We don't yet support the ability to check for new articles while we are
+ // in the middle of subscribing to a feed. For now, abort the check for
+ // new feeds.
+ if (FeedUtils.progressNotifier.mSubscribeMode)
+ {
+ FeedUtils.log.warn("downloadFeed: Aborting RSS New Mail Check. " +
+ "Feed subscription in progress\n");
+ return;
+ }
+
+ let allFolders = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+ if (!aFolder.isServer) {
+ // Add the base folder; it does not get returned by ListDescendants. Do not
+ // add the account folder as it doesn't have the feedUrl property or even
+ // a msgDatabase necessarily.
+ allFolders.appendElement(aFolder, false);
+ }
+
+ aFolder.ListDescendants(allFolders);
+
+ let folder;
+ function* feeder() {
+ let numFolders = allFolders.length;
+ for (let i = 0; i < numFolders; i++) {
+ folder = allFolders.queryElementAt(i, Ci.nsIMsgFolder);
+ FeedUtils.log.debug("downloadFeed: START x/# foldername:uri - " +
+ (i+1) + "/" + numFolders + " " +
+ folder.name + ":" + folder.URI);
+
+ // Ensure folder's msgDatabase is openable for new message processing.
+ // If not, reparse. After the async reparse the folder will be ready
+ // for the next cycle; don't bother with a listener. Continue with
+ // the next folder, as attempting to add a message to a folder with
+ // an unavailable msgDatabase will throw later.
+ if (!FeedUtils.isMsgDatabaseOpenable(folder, true))
+ continue;
+
+ let feedUrlArray = FeedUtils.getFeedUrlsInFolder(folder);
+ // Continue if there are no feedUrls for the folder in the feeds
+ // database. All folders in Trash are skipped.
+ if (!feedUrlArray)
+ continue;
+
+ FeedUtils.log.debug("downloadFeed: CONTINUE foldername:urlArray - " +
+ folder.name + ":" + feedUrlArray);
+
+ FeedUtils.progressNotifier.init(aMsgWindow, false);
+
+ // We need to kick off a download for each feed.
+ let id, feed;
+ for (let url of feedUrlArray)
+ {
+ id = FeedUtils.rdf.GetResource(url);
+ feed = new Feed(id, folder.server);
+ feed.folder = folder;
+ // Bump our pending feed download count.
+ FeedUtils.progressNotifier.mNumPendingFeedDownloads++;
+ feed.download(true, FeedUtils.progressNotifier);
+ FeedUtils.log.debug("downloadFeed: DOWNLOAD feed url - " + url);
+
+ Services.tm.mainThread.dispatch(function() {
+ try {
+ let done = getFeed.next().done;
+ if (done) {
+ // Finished with all feeds in base folder and its subfolders.
+ FeedUtils.log.debug("downloadFeed: Finished with folder - " +
+ aFolder.name);
+ folder = null;
+ allFolders = null;
+ }
+ }
+ catch (ex) {
+ FeedUtils.log.error("downloadFeed: error - " + ex);
+ FeedUtils.progressNotifier.downloaded({name: folder.name}, 0);
+ }
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+
+ yield undefined;
+ }
+ }
+ }
+
+ let getFeed = feeder();
+ try {
+ let done = getFeed.next().done;
+ if (done) {
+ // Nothing to do.
+ FeedUtils.log.debug("downloadFeed: Nothing to do in folder - " +
+ aFolder.name);
+ folder = null;
+ allFolders = null;
+ }
+ }
+ catch (ex) {
+ FeedUtils.log.error("downloadFeed: error - " + ex);
+ FeedUtils.progressNotifier.downloaded({name: aFolder.name}, 0);
+ }
+ },
+
+/**
+ * Subscribe a new feed url.
+ *
+ * @param string aUrl - feed url
+ * @param nsIMsgFolder aFolder - folder
+ * @param nsIDOMWindow aMsgWindow - window
+ */
+ subscribeToFeed: function(aUrl, aFolder, aMsgWindow) {
+ // We don't support the ability to subscribe to several feeds at once yet.
+ // For now, abort the subscription if we are already in the middle of
+ // subscribing to a feed via drag and drop.
+ if (FeedUtils.progressNotifier.mNumPendingFeedDownloads)
+ {
+ FeedUtils.log.warn("subscribeToFeed: Aborting RSS subscription. " +
+ "Feed downloads already in progress\n");
+ return;
+ }
+
+ // If aFolder is null, then use the root folder for the first RSS account.
+ if (!aFolder)
+ aFolder = FeedUtils.getAllRssServerRootFolders()[0];
+
+ // If the user has no Feeds account yet, create one.
+ if (!aFolder)
+ aFolder = FeedUtils.createRssAccount().incomingServer.rootFolder;
+
+ if (!aMsgWindow)
+ {
+ let wlist = Services.wm.getEnumerator("mail:3pane");
+ if (wlist.hasMoreElements())
+ {
+ let win = wlist.getNext().QueryInterface(Ci.nsIDOMWindow);
+ win.focus();
+ aMsgWindow = win.msgWindow;
+ }
+ else
+ {
+ // If there are no open windows, open one, pass it the URL, and
+ // during opening it will subscribe to the feed.
+ let arg = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ arg.data = aUrl;
+ Services.ww.openWindow(null, "chrome://messenger/content/",
+ "_blank", "chrome,dialog=no,all", arg);
+ return;
+ }
+ }
+
+ // If aUrl is a feed url, then it is either of the form
+ // feed://example.org/feed.xml or feed:https://example.org/feed.xml.
+ // Replace feed:// with http:// per the spec, then strip off feed:
+ // for the second case.
+ aUrl = aUrl.replace(/^feed:\x2f\x2f/i, "http://");
+ aUrl = aUrl.replace(/^feed:/i, "");
+
+ // Make sure we aren't already subscribed to this feed before we attempt
+ // to subscribe to it.
+ if (FeedUtils.feedAlreadyExists(aUrl, aFolder.server))
+ {
+ aMsgWindow.statusFeedback.showStatusString(
+ FeedUtils.strings.GetStringFromName("subscribe-feedAlreadySubscribed"));
+ return;
+ }
+
+ let itemResource = FeedUtils.rdf.GetResource(aUrl);
+ let feed = new Feed(itemResource, aFolder.server);
+ feed.quickMode = feed.server.getBoolValue("quickMode");
+ feed.options = FeedUtils.getOptionsAcct(feed.server);
+
+ // If the root server, create a new folder for the feed. The user must
+ // want us to add this subscription url to an existing RSS folder.
+ if (!aFolder.isServer)
+ feed.folder = aFolder;
+
+ FeedUtils.progressNotifier.init(aMsgWindow, true);
+ FeedUtils.progressNotifier.mNumPendingFeedDownloads++;
+ feed.download(true, FeedUtils.progressNotifier);
+ },
+
+/**
+ * Add a feed record to the feeds.rdf database and update the folder's feedUrl
+ * property.
+ *
+ * @param object aFeed - our feed object
+ */
+ addFeed: function(aFeed) {
+ let ds = this.getSubscriptionsDS(aFeed.folder.server);
+ let feeds = this.getSubscriptionsList(ds);
+
+ // Generate a unique ID for the feed.
+ let id = aFeed.url;
+ let i = 1;
+ while (feeds.IndexOf(this.rdf.GetResource(id)) != -1 && ++i < 1000)
+ id = aFeed.url + i;
+ if (i == 1000)
+ throw new Error("FeedUtils.addFeed: couldn't generate a unique ID " +
+ "for feed " + aFeed.url);
+
+ // Add the feed to the list.
+ id = this.rdf.GetResource(id);
+ feeds.AppendElement(id);
+ ds.Assert(id, this.RDF_TYPE, this.FZ_FEED, true);
+ ds.Assert(id, this.DC_IDENTIFIER, this.rdf.GetLiteral(aFeed.url), true);
+ if (aFeed.title)
+ ds.Assert(id, this.DC_TITLE, this.rdf.GetLiteral(aFeed.title), true);
+ ds.Assert(id, this.FZ_DESTFOLDER, aFeed.folder, true);
+ ds.Flush();
+
+ // Update folderpane.
+ this.setFolderPaneProperty(aFeed.folder, "favicon", null, "row");
+ },
+
+/**
+ * Delete a feed record from the feeds.rdf database and update the folder's
+ * feedUrl property.
+ *
+ * @param nsIRDFResource aId - feed url as rdf resource.
+ * @param nsIMsgIncomingServer aServer - folder's account server.
+ * @param nsIMsgFolder aParentFolder - owning folder.
+ */
+ deleteFeed: function(aId, aServer, aParentFolder) {
+ let feed = new Feed(aId, aServer);
+ let ds = this.getSubscriptionsDS(aServer);
+
+ if (!feed || !ds)
+ return;
+
+ // Remove the feed from the subscriptions ds.
+ let feeds = this.getSubscriptionsList(ds);
+ let index = feeds.IndexOf(aId);
+ if (index != -1)
+ feeds.RemoveElementAt(index, false);
+
+ // Remove all assertions about the feed from the subscriptions database.
+ this.removeAssertions(ds, aId);
+ ds.Flush();
+
+ // Remove all assertions about items in the feed from the items database.
+ let itemds = this.getItemsDS(aServer);
+ feed.invalidateItems();
+ feed.removeInvalidItems(true);
+ itemds.Flush();
+
+ // Update folderpane.
+ this.setFolderPaneProperty(aParentFolder, "favicon", null, "row");
+ },
+
+/**
+ * Change an existing feed's url, as identified by FZ_FEED resource in the
+ * feeds.rdf subscriptions database.
+ *
+ * @param obj aFeed - the feed object
+ * @param string aNewUrl - new url
+ * @return bool - true if successful, else false
+ */
+ changeUrlForFeed: function(aFeed, aNewUrl) {
+ if (!aFeed || !aFeed.folder || !aNewUrl)
+ return false;
+
+ if (this.feedAlreadyExists(aNewUrl, aFeed.folder.server))
+ {
+ this.log.info("FeedUtils.changeUrlForFeed: new feed url " + aNewUrl +
+ " already subscribed in account " + aFeed.folder.server.prettyName);
+ return false;
+ }
+
+ let title = aFeed.title;
+ let link = aFeed.link;
+ let quickMode = aFeed.quickMode;
+ let options = aFeed.options;
+
+ this.deleteFeed(this.rdf.GetResource(aFeed.url),
+ aFeed.folder.server, aFeed.folder);
+ aFeed.resource = this.rdf.GetResource(aNewUrl)
+ .QueryInterface(Ci.nsIRDFResource);
+ aFeed.title = title;
+ aFeed.link = link;
+ aFeed.quickMode = quickMode;
+ aFeed.options = options;
+ this.addFeed(aFeed);
+
+ let win = Services.wm.getMostRecentWindow("Mail:News-BlogSubscriptions");
+ if (win)
+ win.FeedSubscriptions.refreshSubscriptionView(aFeed.folder, aNewUrl);
+
+ return true;
+ },
+
+/**
+ * Get the list of feed urls for a folder, as identified by the FZ_DESTFOLDER
+ * tag, directly from the primary feeds.rdf subscriptions database.
+ *
+ * @param nsIMsgFolder - the folder.
+ * @return array of urls, or null if none.
+ */
+ getFeedUrlsInFolder: function(aFolder) {
+ if (aFolder.isServer || aFolder.server.type != "rss" ||
+ aFolder.getFlag(Ci.nsMsgFolderFlags.Trash) ||
+ aFolder.getFlag(Ci.nsMsgFolderFlags.Virtual) ||
+ !aFolder.filePath.exists())
+ // There are never any feedUrls in the account/non-feed/trash/virtual
+ // folders or in a ghost folder (nonexistant on disk yet found in
+ // aFolder.subFolders).
+ return null;
+
+ let feedUrlArray = [];
+
+ // Get the list from the feeds database.
+ try {
+ let ds = this.getSubscriptionsDS(aFolder.server);
+ let enumerator = ds.GetSources(this.FZ_DESTFOLDER, aFolder, true);
+ while (enumerator.hasMoreElements())
+ {
+ let containerArc = enumerator.getNext();
+ let uri = containerArc.QueryInterface(Ci.nsIRDFResource).ValueUTF8;
+ feedUrlArray.push(uri);
+ }
+ }
+ catch(ex)
+ {
+ this.log.error("getFeedUrlsInFolder: feeds.rdf db error - " + ex);
+ this.log.error("getFeedUrlsInFolder: feeds.rdf db error for account - " +
+ aFolder.server.serverURI + " : " + aFolder.server.prettyName);
+ }
+
+ return feedUrlArray.length ? feedUrlArray : null;
+ },
+
+/**
+ * Check if the folder's msgDatabase is openable, reparse if desired.
+ *
+ * @param nsIMsgFolder aFolder - the folder
+ * @param boolean aReparse - reparse if true
+ * @return boolean - true if msgDb is available, else false
+ */
+ isMsgDatabaseOpenable: function(aFolder, aReparse) {
+ let msgDb;
+ try {
+ msgDb = Cc["@mozilla.org/msgDatabase/msgDBService;1"]
+ .getService(Ci.nsIMsgDBService).openFolderDB(aFolder, true);
+ }
+ catch (ex) {}
+
+ if (msgDb)
+ return true;
+
+ if (!aReparse)
+ return false;
+
+ // Force a reparse.
+ FeedUtils.log.debug("checkMsgDb: rebuild msgDatabase for " +
+ aFolder.name + " - " + aFolder.filePath.path);
+ try {
+ // Ignore error returns.
+ aFolder.QueryInterface(Ci.nsIMsgLocalMailFolder)
+ .getDatabaseWithReparse(null, null);
+ }
+ catch (ex) {}
+
+ return false;
+ },
+
+/**
+ * Update a folderpane cached property.
+ *
+ * @param nsIMsgFolder aFolder - folder
+ * @param string aProperty - property
+ * @param string aValue - value
+ * @param string aInvalidate - "row" = folder's row.
+ * "all" = all rows.
+ */
+ setFolderPaneProperty: function(aFolder, aProperty, aValue, aInvalidate) {
+ let win = Services.wm.getMostRecentWindow("mail:3pane");
+ if (!aFolder || !aProperty || !win || !("gFolderTreeView" in win))
+ return;
+
+ win.gFolderTreeView.setFolderCacheProperty(aFolder, aProperty, aValue);
+
+ if (aInvalidate == "all") {
+ win.gFolderTreeView._tree.invalidate();
+ }
+ if (aInvalidate == "row") {
+ let row = win.gFolderTreeView.getIndexOfFolder(aFolder);
+ win.gFolderTreeView._tree.invalidateRow(row);
+ }
+ },
+
+/**
+ * Get the favicon for a feed folder subscription url (first one) or a feed
+ * message url. The favicon service caches it in memory if places history is
+ * not enabled.
+ *
+ * @param nsIMsgFolder aFolder - the feed folder or null if aUrl
+ * @param string aUrl - a url (feed, message, other) or null if aFolder
+ * @param string aIconUrl - the icon url if already determined, else null
+ * @param nsIDOMWindow aWindow - null if requesting url without setting it
+ * @param function aCallback - null or callback
+ * @return string - the favicon url or empty string
+ */
+ getFavicon: function(aFolder, aUrl, aIconUrl, aWindow, aCallback) {
+ // On any error, cache an empty string to show the default favicon, and
+ // don't try anymore in this session.
+ let useDefaultFavicon = (() => {
+ if (aCallback)
+ aCallback("");
+ return "";
+ });
+
+ if (!Services.prefs.getBoolPref("browser.chrome.site_icons") ||
+ !Services.prefs.getBoolPref("browser.chrome.favicons"))
+ return useDefaultFavicon();
+
+ if (aIconUrl != null)
+ return aIconUrl;
+
+ let onLoadSuccess = (aEvent => {
+ let iconUri = Services.io.newURI(aEvent.target.src, null, null);
+ aWindow.specialTabs.mFaviconService.setAndFetchFaviconForPage(
+ uri, iconUri, false,
+ aWindow.specialTabs.mFaviconService.FAVICON_LOAD_NON_PRIVATE,
+ null, Services.scriptSecurityManager.getSystemPrincipal());
+
+ if (aCallback)
+ aCallback(iconUri.spec);
+ });
+
+ let onLoadError = (aEvent => {
+ useDefaultFavicon();
+ let url = aEvent.target.src;
+ aWindow.specialTabs.getFaviconFromPage(url, aCallback);
+ });
+
+ let url = aUrl;
+ if (!url)
+ {
+ // Get the proposed iconUrl from the folder's first subscribed feed's
+ // <link>.
+ if (!aFolder)
+ return useDefaultFavicon();
+
+ let feedUrls = this.getFeedUrlsInFolder(aFolder);
+ url = feedUrls ? feedUrls[0] : null;
+ if (!url)
+ return useDefaultFavicon();
+ }
+
+ if (aFolder)
+ {
+ let ds = this.getSubscriptionsDS(aFolder.server);
+ let resource = this.rdf.GetResource(url).QueryInterface(Ci.nsIRDFResource);
+ let feedLinkUrl = ds.GetTarget(resource, this.RSS_LINK, true);
+ feedLinkUrl = feedLinkUrl ?
+ feedLinkUrl.QueryInterface(Ci.nsIRDFLiteral).Value : null;
+ url = feedLinkUrl && feedLinkUrl.startsWith("http") ? feedLinkUrl : url;
+ }
+
+ let uri, iconUri;
+ try {
+ uri = Services.io.newURI(url, null, null);
+ iconUri = Services.io.newURI(uri.prePath + "/favicon.ico", null, null);
+ }
+ catch (ex) {
+ return useDefaultFavicon();
+ }
+
+ if (!aWindow)
+ return iconUri.spec;
+
+ aWindow.specialTabs.loadFaviconImageNode(onLoadSuccess, onLoadError,
+ iconUri.spec);
+ // Cache the favicon url initially.
+ if (aCallback)
+ aCallback(iconUri.spec);
+
+ return iconUri.spec;
+ },
+
+/**
+ * Update the feeds.rdf database for rename and move/copy folder name changes.
+ *
+ * @param nsIMsgFolder aFolder - the folder, new if rename or target of
+ * move/copy folder (new parent)
+ * @param nsIMsgFolder aOrigFolder - original folder
+ * @param string aAction - "move" or "copy" or "rename"
+ */
+ updateSubscriptionsDS: function(aFolder, aOrigFolder, aAction) {
+ this.log.debug("FeedUtils.updateSubscriptionsDS: " +
+ "\nfolder changed - " + aAction +
+ "\nnew folder - " + aFolder.filePath.path +
+ "\norig folder - " + aOrigFolder.filePath.path);
+
+ if (aFolder.server.type != "rss" || FeedUtils.isInTrash(aOrigFolder))
+ // Target not a feed account folder; nothing to do, or move/rename in
+ // trash; no subscriptions already.
+ return;
+
+ let newFolder = aFolder;
+ let newParentURI = aFolder.URI;
+ let origParentURI = aOrigFolder.URI;
+ if (aAction == "move" || aAction == "copy")
+ {
+ // Get the new folder. Don't process the entire parent (new dest folder)!
+ newFolder = aFolder.getChildNamed(aOrigFolder.name);
+ origParentURI = aOrigFolder.parent ? aOrigFolder.parent.URI :
+ aOrigFolder.rootFolder.URI;
+ }
+
+ this.updateFolderChangeInFeedsDS(newFolder, aOrigFolder, null, null);
+
+ // There may be subfolders, but we only get a single notification; iterate
+ // over all descendent folders of the folder whose location has changed.
+ let newSubFolders = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+ newFolder.ListDescendants(newSubFolders);
+ for (let i = 0; i < newSubFolders.length; i++)
+ {
+ let newSubFolder = newSubFolders.queryElementAt(i, Ci.nsIMsgFolder);
+ FeedUtils.updateFolderChangeInFeedsDS(newSubFolder, aOrigFolder,
+ newParentURI, origParentURI)
+ }
+ },
+
+/**
+ * Update the feeds.rdf database with the new folder's or subfolder's location
+ * for rename and move/copy name changes. The feeds.rdf subscriptions db is
+ * also synced on cross account folder copies. Note that if a copied folder's
+ * url exists in the new account, its active subscription will be switched to
+ * the folder being copied, to enforce the one unique url per account design.
+ *
+ * @param nsIMsgFolder aFolder - new folder
+ * @param nsIMsgFolder aOrigFolder - original folder
+ * @param string aNewAncestorURI - for subfolders, ancestor new folder
+ * @param string aOrigAncestorURI - for subfolders, ancestor original folder
+ */
+ updateFolderChangeInFeedsDS: function(aFolder, aOrigFolder,
+ aNewAncestorURI, aOrigAncestorURI) {
+ this.log.debug("updateFolderChangeInFeedsDS: " +
+ "\naFolder - " + aFolder.URI +
+ "\naOrigFolder - " + aOrigFolder.URI +
+ "\naOrigAncestor - " + aOrigAncestorURI +
+ "\naNewAncestor - " + aNewAncestorURI);
+
+ // Get the original folder's URI.
+ let folderURI = aFolder.URI;
+ let origURI = aNewAncestorURI && aOrigAncestorURI ?
+ folderURI.replace(aNewAncestorURI, aOrigAncestorURI) :
+ aOrigFolder.URI;
+ let origFolderRes = this.rdf.GetResource(origURI);
+ this.log.debug("updateFolderChangeInFeedsDS: urls origURI - " + origURI);
+ // Get the original folder's url list from the feeds database.
+ let feedUrlArray = [];
+ let dsSrc = this.getSubscriptionsDS(aOrigFolder.server);
+ try {
+ let enumerator = dsSrc.GetSources(this.FZ_DESTFOLDER, origFolderRes, true);
+ while (enumerator.hasMoreElements())
+ {
+ let containerArc = enumerator.getNext();
+ let uri = containerArc.QueryInterface(Ci.nsIRDFResource).ValueUTF8;
+ feedUrlArray.push(uri);
+ }
+ }
+ catch(ex)
+ {
+ this.log.error("updateFolderChangeInFeedsDS: feeds.rdf db error for account - " +
+ aOrigFolder.server.prettyName + " : " + ex);
+ }
+
+ if (!feedUrlArray.length)
+ {
+ this.log.debug("updateFolderChangeInFeedsDS: no feedUrls in this folder");
+ return;
+ }
+
+ let id, resource, node;
+ let ds = this.getSubscriptionsDS(aFolder.server);
+ for (let feedUrl of feedUrlArray)
+ {
+ this.log.debug("updateFolderChangeInFeedsDS: feedUrl - " + feedUrl);
+
+ id = this.rdf.GetResource(feedUrl);
+ // If move to trash, unsubscribe.
+ if (this.isInTrash(aFolder))
+ {
+ this.deleteFeed(id, aFolder.server, aFolder);
+ }
+ else
+ {
+ resource = this.rdf.GetResource(aFolder.URI);
+ // Get the node for the current folder URI.
+ node = ds.GetTarget(id, this.FZ_DESTFOLDER, true);
+ if (node)
+ {
+ ds.Change(id, this.FZ_DESTFOLDER, node, resource);
+ }
+ else
+ {
+ // If adding a new feed it's a cross account action; make sure to
+ // preserve all properties from the original datasource where
+ // available. Otherwise use the new folder's name and default server
+ // quickMode; preserve link and options.
+ let feedTitle = dsSrc.GetTarget(id, this.DC_TITLE, true);
+ feedTitle = feedTitle ? feedTitle.QueryInterface(Ci.nsIRDFLiteral).Value :
+ resource.name;
+ let link = dsSrc.GetTarget(id, FeedUtils.RSS_LINK, true);
+ link = link ? link.QueryInterface(Ci.nsIRDFLiteral).Value : "";
+ let quickMode = dsSrc.GetTarget(id, this.FZ_QUICKMODE, true);
+ quickMode = quickMode ? quickMode.QueryInterface(Ci.nsIRDFLiteral).Value :
+ null;
+ quickMode = quickMode == "true" ? true :
+ quickMode == "false" ? false :
+ aFeed.folder.server.getBoolValue("quickMode");
+ let options = dsSrc.GetTarget(id, this.FZ_OPTIONS, true);
+ options = options ? JSON.parse(options.QueryInterface(Ci.nsIRDFLiteral).Value) :
+ this.optionsTemplate;
+
+ let feed = new Feed(id, aFolder.server);
+ feed.folder = aFolder;
+ feed.title = feedTitle;
+ feed.link = link;
+ feed.quickMode = quickMode;
+ feed.options = options;
+ this.addFeed(feed);
+ }
+ }
+ }
+
+ ds.Flush();
+ },
+
+/**
+ * When subscribing to feeds by dnd on, or adding a url to, the account
+ * folder (only), or creating folder structure via opml import, a subfolder is
+ * autocreated and thus the derived/given name must be sanitized to prevent
+ * filesystem errors. Hashing invalid chars based on OS rather than filesystem
+ * is not strictly correct.
+ *
+ * @param nsIMsgFolder aParentFolder - parent folder
+ * @param string aProposedName - proposed name
+ * @param string aDefaultName - default name if proposed sanitizes to
+ * blank, caller ensures sane value
+ * @param bool aUnique - if true, return a unique indexed name.
+ * @return string - sanitized unique name
+ */
+ getSanitizedFolderName: function(aParentFolder, aProposedName, aDefaultName, aUnique) {
+ // Clean up the name for the strictest fs (fat) and to ensure portability.
+ // 1) Replace line breaks and tabs '\n\r\t' with a space.
+ // 2) Remove nonprintable ascii.
+ // 3) Remove invalid win chars '* | \ / : < > ? "'.
+ // 4) Remove all '.' as starting/ending with one is trouble on osx/win.
+ // 5) No leading/trailing spaces.
+ let folderName = aProposedName.replace(/[\n\r\t]+/g, " ")
+ .replace(/[\x00-\x1F]+/g, "")
+ .replace(/[*|\\\/:<>?"]+/g, "")
+ .replace(/[\.]+/g, "")
+ .trim();
+
+ // Prefix with __ if name is:
+ // 1) a reserved win filename.
+ // 2) an undeletable/unrenameable special folder name (bug 259184).
+ if (folderName.toUpperCase()
+ .match(/^COM\d$|^LPT\d$|^CON$|PRN$|^AUX$|^NUL$|^CLOCK\$/) ||
+ folderName.toUpperCase()
+ .match(/^INBOX$|^OUTBOX$|^UNSENT MESSAGES$|^TRASH$/))
+ folderName = "__" + folderName;
+
+ // Use a default if no name is found.
+ if (!folderName)
+ folderName = aDefaultName;
+
+ if (!aUnique)
+ return folderName;
+
+ // Now ensure the folder name is not a dupe; if so append index.
+ let folderNameBase = folderName;
+ let i = 2;
+ while (aParentFolder.containsChildNamed(folderName))
+ {
+ folderName = folderNameBase + "-" + i++;
+ }
+
+ return folderName;
+ },
+
+/**
+ * This object will contain all feed specific properties.
+ */
+ _optionsDefault: {
+ version: 1,
+ // Autotag and <category> handling options.
+ category: {
+ enabled: false,
+ prefixEnabled: false,
+ prefix: null,
+ }
+ },
+
+ get optionsTemplate()
+ {
+ // Copy the object.
+ return JSON.parse(JSON.stringify(this._optionsDefault));
+ },
+
+ getOptionsAcct: function(aServer)
+ {
+ let optionsAcctPref = "mail.server." + aServer.key + ".feed_options";
+ try {
+ return JSON.parse(Services.prefs.getCharPref(optionsAcctPref));
+ }
+ catch (ex) {
+ this.setOptionsAcct(aServer, this._optionsDefault);
+ return JSON.parse(Services.prefs.getCharPref(optionsAcctPref));
+ }
+ },
+
+ setOptionsAcct: function(aServer, aOptions)
+ {
+ let optionsAcctPref = "mail.server." + aServer.key + ".feed_options";
+ let newOptions = this.newOptions(aOptions);
+ Services.prefs.setCharPref(optionsAcctPref, JSON.stringify(newOptions));
+ },
+
+ newOptions: function(aOptions)
+ {
+ // TODO: Clean options, so that only keys in the active template are stored.
+ return aOptions;
+ },
+
+ getSubscriptionsDS: function(aServer) {
+ if (this[aServer.serverURI] && this[aServer.serverURI]["FeedsDS"])
+ return this[aServer.serverURI]["FeedsDS"];
+
+ let file = this.getSubscriptionsFile(aServer);
+ let url = Services.io.getProtocolHandler("file").
+ QueryInterface(Ci.nsIFileProtocolHandler).
+ getURLSpecFromFile(file);
+
+ // GetDataSourceBlocking has a cache, so it's cheap to do this again
+ // once we've already done it once.
+ let ds = this.rdf.GetDataSourceBlocking(url);
+
+ if (!ds)
+ throw new Error("FeedUtils.getSubscriptionsDS: can't get feed " +
+ "subscriptions data source - " + url);
+
+ if (!this[aServer.serverURI])
+ this[aServer.serverURI] = {};
+ return this[aServer.serverURI]["FeedsDS"] =
+ ds.QueryInterface(Ci.nsIRDFRemoteDataSource);
+ },
+
+ getSubscriptionsList: function(aDataSource) {
+ let list = aDataSource.GetTarget(this.FZ_ROOT, this.FZ_FEEDS, true);
+ list = list.QueryInterface(Ci.nsIRDFResource);
+ list = this.rdfContainerUtils.MakeSeq(aDataSource, list);
+ return list;
+ },
+
+ getSubscriptionsFile: function(aServer) {
+ aServer.QueryInterface(Ci.nsIRssIncomingServer);
+ let file = aServer.subscriptionsDataSourcePath;
+
+ // If the file doesn't exist, create it.
+ if (!file.exists())
+ this.createFile(file, this.FEEDS_TEMPLATE);
+
+ return file;
+ },
+
+ FEEDS_TEMPLATE: '<?xml version="1.0"?>\n' +
+ '<RDF:RDF xmlns:dc="http://purl.org/dc/elements/1.1/"\n' +
+ ' xmlns:fz="urn:forumzilla:"\n' +
+ ' xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">\n' +
+ ' <RDF:Description about="urn:forumzilla:root">\n' +
+ ' <fz:feeds>\n' +
+ ' <RDF:Seq>\n' +
+ ' </RDF:Seq>\n' +
+ ' </fz:feeds>\n' +
+ ' </RDF:Description>\n' +
+ '</RDF:RDF>\n',
+
+ getItemsDS: function(aServer) {
+ if (this[aServer.serverURI] && this[aServer.serverURI]["FeedItemsDS"])
+ return this[aServer.serverURI]["FeedItemsDS"];
+
+ let file = this.getItemsFile(aServer);
+ let url = Services.io.getProtocolHandler("file").
+ QueryInterface(Ci.nsIFileProtocolHandler).
+ getURLSpecFromFile(file);
+
+ // GetDataSourceBlocking has a cache, so it's cheap to do this again
+ // once we've already done it once.
+ let ds = this.rdf.GetDataSourceBlocking(url);
+ if (!ds)
+ throw new Error("FeedUtils.getItemsDS: can't get feed items " +
+ "data source - " + url);
+
+ // Note that it this point the datasource may not be loaded yet.
+ // You have to QueryInterface it to nsIRDFRemoteDataSource and check
+ // its "loaded" property to be sure. You can also attach an observer
+ // which will get notified when the load is complete.
+ if (!this[aServer.serverURI])
+ this[aServer.serverURI] = {};
+ return this[aServer.serverURI]["FeedItemsDS"] =
+ ds.QueryInterface(Ci.nsIRDFRemoteDataSource);
+ },
+
+ getItemsFile: function(aServer) {
+ aServer.QueryInterface(Ci.nsIRssIncomingServer);
+ let file = aServer.feedItemsDataSourcePath;
+
+ // If the file doesn't exist, create it.
+ if (!file.exists()) {
+ this.createFile(file, this.FEEDITEMS_TEMPLATE);
+ return file;
+ }
+
+ // If feeditems.rdf is not sane, duplicate messages will occur repeatedly
+ // until the file is corrected; check that the file is valid XML. This is
+ // done lazily only once in a session.
+ let fileUrl = Services.io.getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler)
+ .getURLSpecFromFile(file);
+ let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance(Ci.nsIXMLHttpRequest);
+ request.open("GET", fileUrl, false);
+ request.responseType = "document";
+ request.send();
+ let dom = request.responseXML;
+ if (dom instanceof Ci.nsIDOMXMLDocument &&
+ dom.documentElement.namespaceURI != this.MOZ_PARSERERROR_NS)
+ return file;
+
+ // Error on the file. Rename it and create a new one.
+ this.log.debug("FeedUtils.getItemsFile: error in feeditems.rdf");
+ let errName = "feeditems_error_" +
+ (new Date().toISOString()).replace(/\D/g, "") + ".rdf";
+ file.moveTo(file.parent, errName);
+ file = aServer.feedItemsDataSourcePath;
+ this.createFile(file, this.FEEDITEMS_TEMPLATE);
+ this.log.error("FeedUtils.getItemsFile: error in feeditems.rdf in account '" +
+ aServer.prettyName + "'; the file has been moved to " +
+ errName + " and a new file has been created. Recent messages " +
+ "may be duplicated.");
+ return file;
+ },
+
+ FEEDITEMS_TEMPLATE: '<?xml version="1.0"?>\n' +
+ '<RDF:RDF xmlns:dc="http://purl.org/dc/elements/1.1/"\n' +
+ ' xmlns:fz="urn:forumzilla:"\n' +
+ ' xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">\n' +
+ '</RDF:RDF>\n',
+
+ createFile: function(aFile, aTemplate) {
+ let fos = FileUtils.openSafeFileOutputStream(aFile);
+ fos.write(aTemplate, aTemplate.length);
+ FileUtils.closeSafeFileOutputStream(fos);
+ },
+
+ getParentTargetForChildResource: function(aChildResource, aParentTarget,
+ aServer) {
+ // Generic get feed property, based on child value. Assumes 1 unique
+ // child value with 1 unique parent, valid for feeds.rdf structure.
+ let ds = this.getSubscriptionsDS(aServer);
+ let childRes = this.rdf.GetResource(aChildResource);
+ let parent = null;
+
+ let arcsIn = ds.ArcLabelsIn(childRes);
+ while (arcsIn.hasMoreElements())
+ {
+ let arc = arcsIn.getNext();
+ if (arc instanceof Ci.nsIRDFResource)
+ {
+ parent = ds.GetSource(arc, childRes, true);
+ parent = parent.QueryInterface(Ci.nsIRDFResource);
+ break;
+ }
+ }
+
+ if (parent)
+ {
+ let resource = this.rdf.GetResource(parent.Value);
+ return ds.GetTarget(resource, aParentTarget, true);
+ }
+
+ return null;
+ },
+
+ removeAssertions: function(aDataSource, aResource) {
+ let properties = aDataSource.ArcLabelsOut(aResource);
+ let property;
+ while (properties.hasMoreElements())
+ {
+ property = properties.getNext();
+ let values = aDataSource.GetTargets(aResource, property, true);
+ let value;
+ while (values.hasMoreElements())
+ {
+ value = values.getNext();
+ aDataSource.Unassert(aResource, property, value, true);
+ }
+ }
+ },
+
+/**
+ * Dragging something from somewhere. It may be a nice x-moz-url or from a
+ * browser or app that provides a less nice dataTransfer object in the event.
+ * Extract the url and if it passes the scheme test, try to subscribe.
+ *
+ * @param nsIDOMDataTransfer aDataTransfer - the dnd event's dataTransfer.
+ * @return nsIURI uri - a uri if valid, null if none.
+ */
+ getFeedUriFromDataTransfer: function(aDataTransfer) {
+ let dt = aDataTransfer;
+ let types = ["text/x-moz-url-data", "text/x-moz-url"];
+ let validUri = false;
+ let uri = Cc["@mozilla.org/network/standard-url;1"].
+ createInstance(Ci.nsIURI);
+
+ if (dt.getData(types[0]))
+ {
+ // The url is the data.
+ uri.spec = dt.mozGetDataAt(types[0], 0);
+ validUri = this.isValidScheme(uri);
+ this.log.trace("getFeedUriFromDataTransfer: dropEffect:type:value - " +
+ dt.dropEffect + " : " + types[0] + " : " + uri.spec);
+ }
+ else if (dt.getData(types[1]))
+ {
+ // The url is the first part of the data, the second part is random.
+ uri.spec = dt.mozGetDataAt(types[1], 0).split("\n")[0];
+ validUri = this.isValidScheme(uri);
+ this.log.trace("getFeedUriFromDataTransfer: dropEffect:type:value - " +
+ dt.dropEffect + " : " + types[0] + " : " + uri.spec);
+ }
+ else
+ {
+ // Go through the types and see if there's a url; get the first one.
+ for (let i = 0; i < dt.types.length; i++) {
+ let spec = dt.mozGetDataAt(dt.types[i], 0);
+ this.log.trace("getFeedUriFromDataTransfer: dropEffect:index:type:value - " +
+ dt.dropEffect + " : " + i + " : " + dt.types[i] + " : "+spec);
+ try {
+ uri.spec = spec;
+ validUri = this.isValidScheme(uri);
+ }
+ catch(ex) {}
+
+ if (validUri)
+ break;
+ };
+ }
+
+ return validUri ? uri : null;
+ },
+
+ /**
+ * Returns security/certificate/network error details for an XMLHTTPRequest.
+ *
+ * @param XMLHTTPRequest xhr - The xhr request.
+ * @return array [string errType, string errName] (null if not determined).
+ */
+ createTCPErrorFromFailedXHR: function(xhr) {
+ let status = xhr.channel.QueryInterface(Ci.nsIRequest).status;
+
+ let errType = null;
+ let errName = null;
+ if ((status & 0xff0000) === 0x5a0000) {
+ // Security module.
+ const nsINSSErrorsService = Ci.nsINSSErrorsService;
+ let nssErrorsService = Cc["@mozilla.org/nss_errors_service;1"]
+ .getService(nsINSSErrorsService);
+ let errorClass;
+
+ // getErrorClass()) will throw a generic NS_ERROR_FAILURE if the error
+ // code is somehow not in the set of covered errors.
+ try {
+ errorClass = nssErrorsService.getErrorClass(status);
+ }
+ catch (ex) {
+ // Catch security protocol exception.
+ errorClass = "SecurityProtocol";
+ }
+
+ if (errorClass == nsINSSErrorsService.ERROR_CLASS_BAD_CERT) {
+ errType = "SecurityCertificate";
+ }
+ else {
+ errType = "SecurityProtocol";
+ }
+
+ // NSS_SEC errors (happen below the base value because of negative vals).
+ if ((status & 0xffff) < Math.abs(nsINSSErrorsService.NSS_SEC_ERROR_BASE)) {
+ // The bases are actually negative, so in our positive numeric space,
+ // we need to subtract the base off our value.
+ let nssErr = Math.abs(nsINSSErrorsService.NSS_SEC_ERROR_BASE) - (status & 0xffff);
+
+ switch (nssErr) {
+ case 11: // SEC_ERROR_EXPIRED_CERTIFICATE, sec(11)
+ errName = "SecurityExpiredCertificateError";
+ break;
+ case 12: // SEC_ERROR_REVOKED_CERTIFICATE, sec(12)
+ errName = "SecurityRevokedCertificateError";
+ break;
+
+ // Per bsmith, we will be unable to tell these errors apart very soon,
+ // so it makes sense to just folder them all together already.
+ case 13: // SEC_ERROR_UNKNOWN_ISSUER, sec(13)
+ case 20: // SEC_ERROR_UNTRUSTED_ISSUER, sec(20)
+ case 21: // SEC_ERROR_UNTRUSTED_CERT, sec(21)
+ case 36: // SEC_ERROR_CA_CERT_INVALID, sec(36)
+ errName = "SecurityUntrustedCertificateIssuerError";
+ break;
+ case 90: // SEC_ERROR_INADEQUATE_KEY_USAGE, sec(90)
+ errName = "SecurityInadequateKeyUsageError";
+ break;
+ case 176: // SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED, sec(176)
+ errName = "SecurityCertificateSignatureAlgorithmDisabledError";
+ break;
+ default:
+ errName = "SecurityError";
+ break;
+ }
+ }
+ else {
+ // Calculating the difference.
+ let sslErr = Math.abs(nsINSSErrorsService.NSS_SSL_ERROR_BASE) - (status & 0xffff);
+
+ switch (sslErr) {
+ case 3: // SSL_ERROR_NO_CERTIFICATE, ssl(3)
+ errName = "SecurityNoCertificateError";
+ break;
+ case 4: // SSL_ERROR_BAD_CERTIFICATE, ssl(4)
+ errName = "SecurityBadCertificateError";
+ break;
+ case 8: // SSL_ERROR_UNSUPPORTED_CERTIFICATE_TYPE, ssl(8)
+ errName = "SecurityUnsupportedCertificateTypeError";
+ break;
+ case 9: // SSL_ERROR_UNSUPPORTED_VERSION, ssl(9)
+ errName = "SecurityUnsupportedTLSVersionError";
+ break;
+ case 12: // SSL_ERROR_BAD_CERT_DOMAIN, ssl(12)
+ errName = "SecurityCertificateDomainMismatchError";
+ break;
+ default:
+ errName = "SecurityError";
+ break;
+ }
+ }
+ }
+ else {
+ errType = "Network";
+ switch (status) {
+ // Connect to host:port failed.
+ case 0x804B000C: // NS_ERROR_CONNECTION_REFUSED, network(13)
+ errName = "ConnectionRefusedError";
+ break;
+ // network timeout error.
+ case 0x804B000E: // NS_ERROR_NET_TIMEOUT, network(14)
+ errName = "NetworkTimeoutError";
+ break;
+ // Hostname lookup failed.
+ case 0x804B001E: // NS_ERROR_UNKNOWN_HOST, network(30)
+ errName = "DomainNotFoundError";
+ break;
+ case 0x804B0047: // NS_ERROR_NET_INTERRUPT, network(71)
+ errName = "NetworkInterruptError";
+ break;
+ default:
+ errName = "NetworkError";
+ break;
+ }
+ }
+
+ return [errType, errName];
+ },
+
+/**
+ * Returns if a uri/url is valid to subscribe.
+ *
+ * @param nsIURI aUri or string aUrl - the Uri/Url.
+ * @return boolean - true if a valid scheme, false if not.
+ */
+ _validSchemes: ["http", "https"],
+ isValidScheme: function(aUri) {
+ if (!(aUri instanceof Ci.nsIURI)) {
+ try {
+ aUri = Services.io.newURI(aUri, null, null);
+ }
+ catch (ex) {
+ return false;
+ }
+ }
+
+ return (this._validSchemes.indexOf(aUri.scheme) != -1);
+ },
+
+/**
+ * Is a folder Trash or in Trash.
+ *
+ * @param nsIMsgFolder aFolder - the folder.
+ * @return boolean - true if folder is Trash else false.
+ */
+ isInTrash: function(aFolder) {
+ let trashFolder =
+ aFolder.rootFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Trash);
+ if (trashFolder &&
+ (trashFolder == aFolder || trashFolder.isAncestorOf(aFolder)))
+ return true;
+ return false;
+ },
+
+/**
+ * Return a folder path string constructed from individual folder UTF8 names
+ * stored as properties (not possible hashes used to construct disk foldername).
+ *
+ * @param nsIMsgFolder aFolder - the folder.
+ * @return string prettyName | null - name or null if not a disk folder.
+ */
+ getFolderPrettyPath: function(aFolder) {
+ let msgFolder = MailUtils.getFolderForURI(aFolder.URI, true);
+ if (!msgFolder)
+ // Not a real folder uri.
+ return null;
+
+ if (msgFolder.URI == msgFolder.server.serverURI)
+ return msgFolder.server.prettyName;
+
+ // Server part first.
+ let pathParts = [msgFolder.server.prettyName];
+ let rawPathParts = msgFolder.URI.split(msgFolder.server.serverURI + "/");
+ let folderURI = msgFolder.server.serverURI;
+ rawPathParts = rawPathParts[1].split("/");
+ for (let i = 0; i < rawPathParts.length - 1; i++)
+ {
+ // Two or more folders deep parts here.
+ folderURI += "/" + rawPathParts[i];
+ msgFolder = MailUtils.getFolderForURI(folderURI, true);
+ pathParts.push(msgFolder.name);
+ }
+
+ // Leaf folder last.
+ pathParts.push(aFolder.name);
+ return pathParts.join("/");
+ },
+
+/**
+ * Date validator for feeds.
+ *
+ * @param string aDate - date string
+ * @return boolean - true if passes regex test, false if not
+ */
+ isValidRFC822Date: function(aDate)
+ {
+ const FZ_RFC822_RE = "^(((Mon)|(Tue)|(Wed)|(Thu)|(Fri)|(Sat)|(Sun)), *)?\\d\\d?" +
+ " +((Jan)|(Feb)|(Mar)|(Apr)|(May)|(Jun)|(Jul)|(Aug)|(Sep)|(Oct)|(Nov)|(Dec))" +
+ " +\\d\\d(\\d\\d)? +\\d\\d:\\d\\d(:\\d\\d)? +(([+-]?\\d\\d\\d\\d)|(UT)|(GMT)" +
+ "|(EST)|(EDT)|(CST)|(CDT)|(MST)|(MDT)|(PST)|(PDT)|\\w)$";
+ let regex = new RegExp(FZ_RFC822_RE);
+ return regex.test(aDate);
+ },
+
+/**
+ * Create rfc5322 date.
+ *
+ * @param [string] aDateString - optional date string; if null or invalid
+ * date, get the current datetime.
+ * @return string - an rfc5322 date string
+ */
+ getValidRFC5322Date: function(aDateString)
+ {
+ let d = new Date(aDateString || new Date().getTime());
+ d = isNaN(d.getTime()) ? new Date() : d;
+ return jsmime.headeremitter.emitStructuredHeader("Date", d, {}).substring(6).trim();
+ },
+
+ // Progress glue code. Acts as a go between the RSS back end and the mail
+ // window front end determined by the aMsgWindow parameter passed into
+ // nsINewsBlogFeedDownloader.
+ progressNotifier: {
+ mSubscribeMode: false,
+ mMsgWindow: null,
+ mStatusFeedback: null,
+ mFeeds: {},
+ // Keeps track of the total number of feeds we have been asked to download.
+ // This number may not reflect the # of entries in our mFeeds array because
+ // not all feeds may have reported in for the first time.
+ mNumPendingFeedDownloads: 0,
+
+ init: function(aMsgWindow, aSubscribeMode)
+ {
+ if (!this.mNumPendingFeedDownloads)
+ {
+ // If we aren't already in the middle of downloading feed items.
+ this.mStatusFeedback = aMsgWindow ? aMsgWindow.statusFeedback : null;
+ this.mSubscribeMode = aSubscribeMode;
+ this.mMsgWindow = aMsgWindow;
+
+ if (this.mStatusFeedback)
+ {
+ this.mStatusFeedback.startMeteors();
+ this.mStatusFeedback.showStatusString(
+ FeedUtils.strings.GetStringFromName(
+ aSubscribeMode ? "subscribe-validating-feed" :
+ "newsblog-getNewMsgsCheck"));
+ }
+ }
+ },
+
+ downloaded: function(feed, aErrorCode)
+ {
+ let location = feed.folder ? feed.folder.filePath.path : "";
+ FeedUtils.log.debug("downloaded: "+
+ (this.mSubscribeMode ? "Subscribe " : "Update ") +
+ "errorCode:feedName:folder - " +
+ aErrorCode + " : " + feed.name + " : " + location);
+ if (this.mSubscribeMode)
+ {
+ if (aErrorCode == FeedUtils.kNewsBlogSuccess)
+ {
+ // Add the feed to the databases.
+ FeedUtils.addFeed(feed);
+
+ // Nice touch: select the folder that now contains the newly subscribed
+ // feed. This is particularly nice if we just finished subscribing
+ // to a feed URL that the operating system gave us.
+ this.mMsgWindow.windowCommands.selectFolder(feed.folder.URI);
+
+ // Check for an existing feed subscriptions window and update it.
+ let subscriptionsWindow =
+ Services.wm.getMostRecentWindow("Mail:News-BlogSubscriptions");
+ if (subscriptionsWindow)
+ subscriptionsWindow.FeedSubscriptions.
+ FolderListener.folderAdded(feed.folder);
+ }
+ else
+ {
+ // Non success. Remove intermediate traces from the feeds database.
+ if (feed && feed.url && feed.server)
+ FeedUtils.deleteFeed(FeedUtils.rdf.GetResource(feed.url),
+ feed.server,
+ feed.server.rootFolder);
+ }
+ }
+
+ if (feed.folder && aErrorCode != FeedUtils.kNewsBlogFeedIsBusy)
+ // Free msgDatabase after new mail biff is set; if busy let the next
+ // result do the freeing. Otherwise new messages won't be indicated.
+ feed.folder.msgDatabase = null;
+
+ let message = "";
+ if (feed.folder)
+ location = FeedUtils.getFolderPrettyPath(feed.folder) + " -> ";
+ switch (aErrorCode) {
+ case FeedUtils.kNewsBlogSuccess:
+ case FeedUtils.kNewsBlogFeedIsBusy:
+ message = "";
+ break;
+ case FeedUtils.kNewsBlogNoNewItems:
+ message = feed.url+". " +
+ FeedUtils.strings.GetStringFromName(
+ "newsblog-noNewArticlesForFeed");
+ break;
+ case FeedUtils.kNewsBlogInvalidFeed:
+ message = FeedUtils.strings.formatStringFromName(
+ "newsblog-feedNotValid", [feed.url], 1);
+ break;
+ case FeedUtils.kNewsBlogRequestFailure:
+ message = FeedUtils.strings.formatStringFromName(
+ "newsblog-networkError", [feed.url], 1);
+ break;
+ case FeedUtils.kNewsBlogFileError:
+ message = FeedUtils.strings.GetStringFromName(
+ "subscribe-errorOpeningFile");
+ break;
+ case FeedUtils.kNewsBlogBadCertError:
+ let host = Services.io.newURI(feed.url, null, null).host;
+ message = FeedUtils.strings.formatStringFromName(
+ "newsblog-badCertError", [host], 1);
+ break;
+ case FeedUtils.kNewsBlogNoAuthError:
+ message = FeedUtils.strings.formatStringFromName(
+ "newsblog-noAuthError", [feed.url], 1);
+ break;
+ }
+ if (message)
+ FeedUtils.log.info("downloaded: " +
+ (this.mSubscribeMode ? "Subscribe: " : "Update: ") +
+ location + message);
+
+ if (this.mStatusFeedback)
+ {
+ this.mStatusFeedback.showStatusString(message);
+ this.mStatusFeedback.stopMeteors();
+ }
+
+ if (!--this.mNumPendingFeedDownloads)
+ {
+ FeedUtils.getSubscriptionsDS(feed.server).Flush();
+ this.mFeeds = {};
+ this.mSubscribeMode = false;
+ FeedUtils.log.debug("downloaded: all pending downloads finished");
+
+ // Should we do this on a timer so the text sticks around for a little
+ // while? It doesnt look like we do it on a timer for newsgroups so
+ // we'll follow that model. Don't clear the status text if we just
+ // dumped an error to the status bar!
+ if (aErrorCode == FeedUtils.kNewsBlogSuccess && this.mStatusFeedback)
+ this.mStatusFeedback.showStatusString("");
+ }
+
+ feed = null;
+ },
+
+ // This gets called after the RSS parser finishes storing a feed item to
+ // disk. aCurrentFeedItems is an integer corresponding to how many feed
+ // items have been downloaded so far. aMaxFeedItems is an integer
+ // corresponding to the total number of feed items to download
+ onFeedItemStored: function (feed, aCurrentFeedItems, aMaxFeedItems)
+ {
+ // We currently don't do anything here. Eventually we may add status
+ // text about the number of new feed articles received.
+
+ if (this.mSubscribeMode && this.mStatusFeedback)
+ {
+ // If we are subscribing to a feed, show feed download progress.
+ this.mStatusFeedback.showStatusString(
+ FeedUtils.strings.formatStringFromName("subscribe-gettingFeedItems",
+ [aCurrentFeedItems, aMaxFeedItems], 2));
+ this.onProgress(feed, aCurrentFeedItems, aMaxFeedItems);
+ }
+ },
+
+ onProgress: function(feed, aProgress, aProgressMax, aLengthComputable)
+ {
+ if (feed.url in this.mFeeds)
+ // Have we already seen this feed?
+ this.mFeeds[feed.url].currentProgress = aProgress;
+ else
+ this.mFeeds[feed.url] = {currentProgress: aProgress,
+ maxProgress: aProgressMax};
+
+ this.updateProgressBar();
+ },
+
+ updateProgressBar: function()
+ {
+ let currentProgress = 0;
+ let maxProgress = 0;
+ for (let index in this.mFeeds)
+ {
+ currentProgress += this.mFeeds[index].currentProgress;
+ maxProgress += this.mFeeds[index].maxProgress;
+ }
+
+ // If we start seeing weird "jumping" behavior where the progress bar
+ // goes below a threshold then above it again, then we can factor a
+ // fudge factor here based on the number of feeds that have not reported
+ // yet and the avg progress we've already received for existing feeds.
+ // Fortunately the progressmeter is on a timer and only updates every so
+ // often. For the most part all of our request have initial progress
+ // before the UI actually picks up a progress value.
+ if (this.mStatusFeedback)
+ {
+ let progress = (currentProgress * 100) / maxProgress;
+ this.mStatusFeedback.showProgress(progress);
+ }
+ }
+ }
+};
+
+XPCOMUtils.defineLazyGetter(FeedUtils, "log", function() {
+ return Log4Moz.getConfiguredLogger("Feeds");
+});
+
+XPCOMUtils.defineLazyGetter(FeedUtils, "strings", function() {
+ return Services.strings.createBundle(
+ "chrome://messenger-newsblog/locale/newsblog.properties");
+});
+
+XPCOMUtils.defineLazyGetter(FeedUtils, "rdf", function() {
+ return Cc["@mozilla.org/rdf/rdf-service;1"].
+ getService(Ci.nsIRDFService);
+});
+
+XPCOMUtils.defineLazyGetter(FeedUtils, "rdfContainerUtils", function() {
+ return Cc["@mozilla.org/rdf/container-utils;1"].
+ getService(Ci.nsIRDFContainerUtils);
+});