summaryrefslogtreecommitdiffstats
path: root/mailnews/extensions/newsblog/content
diff options
context:
space:
mode:
Diffstat (limited to 'mailnews/extensions/newsblog/content')
-rw-r--r--mailnews/extensions/newsblog/content/Feed.js620
-rw-r--r--mailnews/extensions/newsblog/content/FeedItem.js490
-rw-r--r--mailnews/extensions/newsblog/content/FeedUtils.jsm1608
-rw-r--r--mailnews/extensions/newsblog/content/am-newsblog.js63
-rw-r--r--mailnews/extensions/newsblog/content/am-newsblog.xul155
-rw-r--r--mailnews/extensions/newsblog/content/feed-parser.js1034
-rw-r--r--mailnews/extensions/newsblog/content/feed-subscriptions.js2703
-rw-r--r--mailnews/extensions/newsblog/content/feed-subscriptions.xul235
-rw-r--r--mailnews/extensions/newsblog/content/feedAccountWizard.js45
-rw-r--r--mailnews/extensions/newsblog/content/feedAccountWizard.xul79
-rw-r--r--mailnews/extensions/newsblog/content/newsblogOverlay.js363
11 files changed, 7395 insertions, 0 deletions
diff --git a/mailnews/extensions/newsblog/content/Feed.js b/mailnews/extensions/newsblog/content/Feed.js
new file mode 100644
index 000000000..7e47260a8
--- /dev/null
+++ b/mailnews/extensions/newsblog/content/Feed.js
@@ -0,0 +1,620 @@
+/* -*- 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/. */
+
+// Cache for all of the feeds currently being downloaded, indexed by URL,
+// so the load event listener can access the Feed objects after it finishes
+// downloading the feed.
+var FeedCache =
+{
+ mFeeds: {},
+
+ putFeed: function (aFeed)
+ {
+ this.mFeeds[this.normalizeHost(aFeed.url)] = aFeed;
+ },
+
+ getFeed: function (aUrl)
+ {
+ let index = this.normalizeHost(aUrl);
+ if (index in this.mFeeds)
+ return this.mFeeds[index];
+
+ return null;
+ },
+
+ removeFeed: function (aUrl)
+ {
+ let index = this.normalizeHost(aUrl);
+ if (index in this.mFeeds)
+ delete this.mFeeds[index];
+ },
+
+ normalizeHost: function (aUrl)
+ {
+ try
+ {
+ let normalizedUrl = Services.io.newURI(aUrl, null, null);
+ normalizedUrl.host = normalizedUrl.host.toLowerCase();
+ return normalizedUrl.spec
+ }
+ catch (ex)
+ {
+ return aUrl;
+ }
+ }
+};
+
+function Feed(aResource, aRSSServer)
+{
+ this.resource = aResource.QueryInterface(Ci.nsIRDFResource);
+ this.server = aRSSServer;
+}
+
+Feed.prototype =
+{
+ description: null,
+ author: null,
+ request: null,
+ server: null,
+ downloadCallback: null,
+ resource: null,
+ items: new Array(),
+ itemsStored: 0,
+ mFolder: null,
+ mInvalidFeed: false,
+ mFeedType: null,
+ mLastModified: null,
+
+ get folder()
+ {
+ return this.mFolder;
+ },
+
+ set folder (aFolder)
+ {
+ this.mFolder = aFolder;
+ },
+
+ get name()
+ {
+ // Used for the feed's title in Subcribe dialog and opml export.
+ let name = this.title || this.description || this.url;
+ return name.replace(/[\n\r\t]+/g, " ").replace(/[\x00-\x1F]+/g, "");
+ },
+
+ get folderName()
+ {
+ if (this.mFolderName)
+ return this.mFolderName;
+
+ // Get a unique sanitized name. Use title or description as a base;
+ // these are mandatory by spec. Length of 80 is plenty.
+ let folderName = (this.title || this.description || "").substr(0,80);
+ let defaultName = FeedUtils.strings.GetStringFromName("ImportFeedsNew");
+ return this.mFolderName = FeedUtils.getSanitizedFolderName(this.server.rootMsgFolder,
+ folderName,
+ defaultName,
+ true);
+ },
+
+ download: function(aParseItems, aCallback)
+ {
+ // May be null.
+ this.downloadCallback = aCallback;
+
+ // Whether or not to parse items when downloading and parsing the feed.
+ // Defaults to true, but setting to false is useful for obtaining
+ // just the title of the feed when the user subscribes to it.
+ this.parseItems = aParseItems == null ? true : aParseItems ? true : false;
+
+ // Before we do anything, make sure the url is an http url. This is just
+ // a sanity check so we don't try opening mailto urls, imap urls, etc. that
+ // the user may have tried to subscribe to as an rss feed.
+ if (!FeedUtils.isValidScheme(this.url))
+ {
+ // Simulate an invalid feed error.
+ FeedUtils.log.info("Feed.download: invalid protocol for - " + this.url);
+ this.onParseError(this);
+ return;
+ }
+
+ // Before we try to download the feed, make sure we aren't already
+ // processing the feed by looking up the url in our feed cache.
+ if (FeedCache.getFeed(this.url))
+ {
+ if (this.downloadCallback)
+ this.downloadCallback.downloaded(this, FeedUtils.kNewsBlogFeedIsBusy);
+ // Return, the feed is already in use.
+ return;
+ }
+
+ if (Services.io.offline) {
+ // If offline and don't want to go online, just add the feed subscription;
+ // it can be verified later (the folder name will be the url if not adding
+ // to an existing folder). Only for subscribe actions; passive biff and
+ // active get new messages are handled prior to getting here.
+ let win = Services.wm.getMostRecentWindow("mail:3pane");
+ if (!win.MailOfflineMgr.getNewMail()) {
+ this.storeNextItem();
+ return;
+ }
+ }
+
+ this.request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+ createInstance(Ci.nsIXMLHttpRequest);
+ // Must set onProgress before calling open.
+ this.request.onprogress = this.onProgress;
+ this.request.open("GET", this.url, true);
+ this.request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE |
+ Ci.nsIRequest.INHIBIT_CACHING;
+
+ // Some servers, if sent If-Modified-Since, will send 304 if subsequently
+ // not sent If-Modified-Since, as in the case of an unsubscribe and new
+ // subscribe. Send start of century date to force a download; some servers
+ // will 304 on older dates (such as epoch 1970).
+ let lastModified = this.lastModified || "Sat, 01 Jan 2000 00:00:00 GMT";
+ this.request.setRequestHeader("If-Modified-Since", lastModified);
+
+ // Only order what you're going to eat...
+ this.request.responseType = "document";
+ this.request.overrideMimeType("text/xml");
+ this.request.setRequestHeader("Accept", FeedUtils.REQUEST_ACCEPT);
+ this.request.timeout = FeedUtils.REQUEST_TIMEOUT;
+ this.request.onload = this.onDownloaded;
+ this.request.onerror = this.onDownloadError;
+ this.request.ontimeout = this.onDownloadError;
+ FeedCache.putFeed(this);
+ this.request.send(null);
+ },
+
+ onDownloaded: function(aEvent)
+ {
+ let request = aEvent.target;
+ let isHttp = request.channel.originalURI.scheme.startsWith("http");
+ let url = request.channel.originalURI.spec;
+ if (isHttp && (request.status < 200 || request.status >= 300))
+ {
+ Feed.prototype.onDownloadError(aEvent);
+ return;
+ }
+
+ FeedUtils.log.debug("Feed.onDownloaded: got a download - " + url);
+ let feed = FeedCache.getFeed(url);
+ if (!feed)
+ throw new Error("Feed.onDownloaded: error - couldn't retrieve feed " +
+ "from cache");
+
+ // If the server sends a Last-Modified header, store the property on the
+ // feed so we can use it when making future requests, to avoid downloading
+ // and parsing feeds that have not changed. Don't update if merely checking
+ // the url, as for subscribe move/copy, as a subsequent refresh may get a 304.
+ // Save the response and persist it only upon successful completion of the
+ // refresh cycle (i.e. not if the request is cancelled).
+ let lastModifiedHeader = request.getResponseHeader("Last-Modified");
+ feed.mLastModified = (lastModifiedHeader && feed.parseItems) ?
+ lastModifiedHeader : null;
+
+ // The download callback is called asynchronously when parse() is done.
+ feed.parse();
+ },
+
+ onProgress: function(aEvent)
+ {
+ let request = aEvent.target;
+ let url = request.channel.originalURI.spec;
+ let feed = FeedCache.getFeed(url);
+
+ if (feed.downloadCallback)
+ feed.downloadCallback.onProgress(feed, aEvent.loaded, aEvent.total,
+ aEvent.lengthComputable);
+ },
+
+ onDownloadError: function(aEvent)
+ {
+ let request = aEvent.target;
+ let url = request.channel.originalURI.spec;
+ let feed = FeedCache.getFeed(url);
+ if (feed.downloadCallback)
+ {
+ // Generic network or 'not found' error initially.
+ let error = FeedUtils.kNewsBlogRequestFailure;
+
+ if (request.status == 304) {
+ // If the http status code is 304, the feed has not been modified
+ // since we last downloaded it and does not need to be parsed.
+ error = FeedUtils.kNewsBlogNoNewItems;
+ }
+ else {
+ let [errType, errName] = FeedUtils.createTCPErrorFromFailedXHR(request);
+ FeedUtils.log.info("Feed.onDownloaded: request errType:errName:statusCode - " +
+ errType + ":" + errName + ":" + request.status);
+ if (errType == "SecurityCertificate")
+ // This is the code for nsINSSErrorsService.ERROR_CLASS_BAD_CERT
+ // overrideable security certificate errors.
+ error = FeedUtils.kNewsBlogBadCertError;
+
+ if (request.status == 401 || request.status == 403)
+ // Unauthorized or Forbidden.
+ error = FeedUtils.kNewsBlogNoAuthError;
+ }
+
+ feed.downloadCallback.downloaded(feed, error);
+ }
+
+ FeedCache.removeFeed(url);
+ },
+
+ onParseError: function(aFeed)
+ {
+ if (!aFeed)
+ return;
+
+ aFeed.mInvalidFeed = true;
+ if (aFeed.downloadCallback)
+ aFeed.downloadCallback.downloaded(aFeed, FeedUtils.kNewsBlogInvalidFeed);
+
+ FeedCache.removeFeed(aFeed.url);
+ },
+
+ onUrlChange: function(aFeed, aOldUrl)
+ {
+ if (!aFeed)
+ return;
+
+ // Simulate a cancel after a url update; next cycle will check the new url.
+ aFeed.mInvalidFeed = true;
+ if (aFeed.downloadCallback)
+ aFeed.downloadCallback.downloaded(aFeed, FeedUtils.kNewsBlogCancel);
+
+ FeedCache.removeFeed(aOldUrl);
+ },
+
+ get url()
+ {
+ let ds = FeedUtils.getSubscriptionsDS(this.server);
+ let url = ds.GetTarget(this.resource, FeedUtils.DC_IDENTIFIER, true);
+ if (url)
+ url = url.QueryInterface(Ci.nsIRDFLiteral).Value;
+ else
+ url = this.resource.ValueUTF8;
+
+ return url;
+ },
+
+ get title()
+ {
+ let ds = FeedUtils.getSubscriptionsDS(this.server);
+ let title = ds.GetTarget(this.resource, FeedUtils.DC_TITLE, true);
+ if (title)
+ title = title.QueryInterface(Ci.nsIRDFLiteral).Value;
+
+ return title;
+ },
+
+ set title (aNewTitle)
+ {
+ if (!aNewTitle)
+ return;
+
+ let ds = FeedUtils.getSubscriptionsDS(this.server);
+ aNewTitle = FeedUtils.rdf.GetLiteral(aNewTitle);
+ let old_title = ds.GetTarget(this.resource, FeedUtils.DC_TITLE, true);
+ if (old_title)
+ ds.Change(this.resource, FeedUtils.DC_TITLE, old_title, aNewTitle);
+ else
+ ds.Assert(this.resource, FeedUtils.DC_TITLE, aNewTitle, true);
+ },
+
+ get lastModified()
+ {
+ let ds = FeedUtils.getSubscriptionsDS(this.server);
+ let lastModified = ds.GetTarget(this.resource,
+ FeedUtils.DC_LASTMODIFIED,
+ true);
+ if (lastModified)
+ lastModified = lastModified.QueryInterface(Ci.nsIRDFLiteral).Value;
+
+ return lastModified;
+ },
+
+ set lastModified(aLastModified)
+ {
+ let ds = FeedUtils.getSubscriptionsDS(this.server);
+ aLastModified = FeedUtils.rdf.GetLiteral(aLastModified);
+ let old_lastmodified = ds.GetTarget(this.resource,
+ FeedUtils.DC_LASTMODIFIED,
+ true);
+ if (old_lastmodified)
+ ds.Change(this.resource, FeedUtils.DC_LASTMODIFIED,
+ old_lastmodified, aLastModified);
+ else
+ ds.Assert(this.resource, FeedUtils.DC_LASTMODIFIED, aLastModified, true);
+ },
+
+ get quickMode ()
+ {
+ let ds = FeedUtils.getSubscriptionsDS(this.server);
+ let quickMode = ds.GetTarget(this.resource, FeedUtils.FZ_QUICKMODE, true);
+ if (quickMode)
+ {
+ quickMode = quickMode.QueryInterface(Ci.nsIRDFLiteral);
+ quickMode = quickMode.Value == "true";
+ }
+
+ return quickMode;
+ },
+
+ set quickMode (aNewQuickMode)
+ {
+ let ds = FeedUtils.getSubscriptionsDS(this.server);
+ aNewQuickMode = FeedUtils.rdf.GetLiteral(aNewQuickMode);
+ let old_quickMode = ds.GetTarget(this.resource,
+ FeedUtils.FZ_QUICKMODE,
+ true);
+ if (old_quickMode)
+ ds.Change(this.resource, FeedUtils.FZ_QUICKMODE,
+ old_quickMode, aNewQuickMode);
+ else
+ ds.Assert(this.resource, FeedUtils.FZ_QUICKMODE,
+ aNewQuickMode, true);
+ },
+
+ get options ()
+ {
+ let ds = FeedUtils.getSubscriptionsDS(this.server);
+ let options = ds.GetTarget(this.resource, FeedUtils.FZ_OPTIONS, true);
+ if (options)
+ return JSON.parse(options.QueryInterface(Ci.nsIRDFLiteral).Value);
+
+ return null;
+ },
+
+ set options (aOptions)
+ {
+ let newOptions = aOptions ? FeedUtils.newOptions(aOptions) :
+ FeedUtils._optionsDefault;
+ let ds = FeedUtils.getSubscriptionsDS(this.server);
+ newOptions = FeedUtils.rdf.GetLiteral(JSON.stringify(newOptions));
+ let oldOptions = ds.GetTarget(this.resource, FeedUtils.FZ_OPTIONS, true);
+ if (oldOptions)
+ ds.Change(this.resource, FeedUtils.FZ_OPTIONS, oldOptions, newOptions);
+ else
+ ds.Assert(this.resource, FeedUtils.FZ_OPTIONS, newOptions, true);
+ },
+
+ categoryPrefs: function ()
+ {
+ let categoryPrefsAcct = FeedUtils.getOptionsAcct(this.server).category;
+ if (!this.options)
+ return categoryPrefsAcct;
+
+ return this.options.category;
+ },
+
+ get link ()
+ {
+ let ds = FeedUtils.getSubscriptionsDS(this.server);
+ let link = ds.GetTarget(this.resource, FeedUtils.RSS_LINK, true);
+ if (link)
+ link = link.QueryInterface(Ci.nsIRDFLiteral).Value;
+
+ return link;
+ },
+
+ set link (aNewLink)
+ {
+ if (!aNewLink)
+ return;
+
+ let ds = FeedUtils.getSubscriptionsDS(this.server);
+ aNewLink = FeedUtils.rdf.GetLiteral(aNewLink);
+ let old_link = ds.GetTarget(this.resource, FeedUtils.RSS_LINK, true);
+ if (old_link)
+ ds.Change(this.resource, FeedUtils.RSS_LINK, old_link, aNewLink);
+ else
+ ds.Assert(this.resource, FeedUtils.RSS_LINK, aNewLink, true);
+ },
+
+ parse: function()
+ {
+ // Create a feed parser which will parse the feed.
+ let parser = new FeedParser();
+ this.itemsToStore = parser.parseFeed(this, this.request.responseXML);
+ parser = null;
+
+ if (this.mInvalidFeed)
+ {
+ this.request = null;
+ this.mInvalidFeed = false;
+ return;
+ }
+
+ // storeNextItem() will iterate through the parsed items, storing each one.
+ this.itemsToStoreIndex = 0;
+ this.itemsStored = 0;
+ this.storeNextItem();
+ },
+
+ invalidateItems: function ()
+ {
+ let ds = FeedUtils.getItemsDS(this.server);
+ FeedUtils.log.debug("Feed.invalidateItems: for url - " + this.url);
+ let items = ds.GetSources(FeedUtils.FZ_FEED, this.resource, true);
+ let item;
+
+ while (items.hasMoreElements())
+ {
+ item = items.getNext();
+ item = item.QueryInterface(Ci.nsIRDFResource);
+ FeedUtils.log.trace("Feed.invalidateItems: item - " + item.Value);
+ let valid = ds.GetTarget(item, FeedUtils.FZ_VALID, true);
+ if (valid)
+ ds.Unassert(item, FeedUtils.FZ_VALID, valid, true);
+ }
+ },
+
+ removeInvalidItems: function(aDeleteFeed)
+ {
+ let ds = FeedUtils.getItemsDS(this.server);
+ FeedUtils.log.debug("Feed.removeInvalidItems: for url - " + this.url);
+ let items = ds.GetSources(FeedUtils.FZ_FEED, this.resource, true);
+ let item;
+ let currentTime = new Date().getTime();
+ while (items.hasMoreElements())
+ {
+ item = items.getNext();
+ item = item.QueryInterface(Ci.nsIRDFResource);
+
+ if (ds.HasAssertion(item, FeedUtils.FZ_VALID,
+ FeedUtils.RDF_LITERAL_TRUE, true))
+ continue;
+
+ let lastSeenTime = ds.GetTarget(item, FeedUtils.FZ_LAST_SEEN_TIMESTAMP, true);
+ if (lastSeenTime)
+ lastSeenTime = parseInt(lastSeenTime.QueryInterface(Ci.nsIRDFLiteral).Value)
+ else
+ lastSeenTime = 0;
+
+ if ((currentTime - lastSeenTime) < FeedUtils.INVALID_ITEM_PURGE_DELAY &&
+ !aDeleteFeed)
+ // Don't immediately purge items in active feeds; do so for deleted feeds.
+ continue;
+
+ FeedUtils.log.trace("Feed.removeInvalidItems: item - " + item.Value);
+ ds.Unassert(item, FeedUtils.FZ_FEED, this.resource, true);
+ if (ds.hasArcOut(item, FeedUtils.FZ_FEED))
+ FeedUtils.log.debug("Feed.removeInvalidItems: " + item.Value +
+ " is from more than one feed; only the reference to" +
+ " this feed removed");
+ else
+ FeedUtils.removeAssertions(ds, item);
+ }
+ },
+
+ createFolder: function()
+ {
+ if (this.folder)
+ return;
+
+ try {
+ this.folder = this.server.rootMsgFolder
+ .QueryInterface(Ci.nsIMsgLocalMailFolder)
+ .createLocalSubfolder(this.folderName);
+ }
+ catch (ex) {
+ // An error creating.
+ FeedUtils.log.info("Feed.createFolder: error creating folder - '"+
+ this.folderName+"' in parent folder "+
+ this.server.rootMsgFolder.filePath.path + " -- "+ex);
+ // But its remnants are still there, clean up.
+ let xfolder = this.server.rootMsgFolder.getChildNamed(this.folderName);
+ this.server.rootMsgFolder.propagateDelete(xfolder, true, null);
+ }
+ },
+
+ // Gets the next item from itemsToStore and forces that item to be stored
+ // to the folder. If more items are left to be stored, fires a timer for
+ // the next one, otherwise triggers a download done notification to the UI.
+ storeNextItem: function()
+ {
+ if (FeedUtils.CANCEL_REQUESTED)
+ {
+ FeedUtils.CANCEL_REQUESTED = false;
+ this.cleanupParsingState(this, FeedUtils.kNewsBlogCancel);
+ return;
+ }
+
+ if (!this.itemsToStore || !this.itemsToStore.length)
+ {
+ let code = FeedUtils.kNewsBlogSuccess;
+ this.createFolder();
+ if (!this.folder)
+ code = FeedUtils.kNewsBlogFileError;
+ this.cleanupParsingState(this, code);
+ return;
+ }
+
+ let item = this.itemsToStore[this.itemsToStoreIndex];
+
+ if (item.store())
+ this.itemsStored++;
+
+ if (!this.folder)
+ {
+ this.cleanupParsingState(this, FeedUtils.kNewsBlogFileError);
+ return;
+ }
+
+ this.itemsToStoreIndex++;
+
+ // If the listener is tracking progress for each item, report it here.
+ if (item.feed.downloadCallback && item.feed.downloadCallback.onFeedItemStored)
+ item.feed.downloadCallback.onFeedItemStored(item.feed,
+ this.itemsToStoreIndex,
+ this.itemsToStore.length);
+
+ // Eventually we'll report individual progress here.
+
+ if (this.itemsToStoreIndex < this.itemsToStore.length)
+ {
+ if (!this.storeItemsTimer)
+ this.storeItemsTimer = Cc["@mozilla.org/timer;1"].
+ createInstance(Ci.nsITimer);
+ this.storeItemsTimer.initWithCallback(this, 50, Ci.nsITimer.TYPE_ONE_SHOT);
+ }
+ else
+ {
+ // We have just finished downloading one or more feed items into the
+ // destination folder; if the folder is still listed as having new
+ // messages in it, then we should set the biff state on the folder so the
+ // right RDF UI changes happen in the folder pane to indicate new mail.
+ if (item.feed.folder.hasNewMessages)
+ {
+ item.feed.folder.biffState = Ci.nsIMsgFolder.nsMsgBiffState_NewMail;
+ // Run the bayesian spam filter, if enabled.
+ item.feed.folder.callFilterPlugins(null);
+ }
+
+ this.cleanupParsingState(this, FeedUtils.kNewsBlogSuccess);
+ }
+ },
+
+ cleanupParsingState: function(aFeed, aCode)
+ {
+ // Now that we are done parsing the feed, remove the feed from the cache.
+ FeedCache.removeFeed(aFeed.url);
+
+ if (aFeed.parseItems)
+ {
+ // Do this only if we're in parse/store mode.
+ aFeed.removeInvalidItems(false);
+
+ if (aCode == FeedUtils.kNewsBlogSuccess && aFeed.mLastModified)
+ aFeed.lastModified = aFeed.mLastModified;
+
+ // Flush any feed item changes to disk.
+ let ds = FeedUtils.getItemsDS(aFeed.server);
+ ds.Flush();
+ FeedUtils.log.debug("Feed.cleanupParsingState: items stored - " + this.itemsStored);
+ }
+
+ // Force the xml http request to go away. This helps reduce some nasty
+ // assertions on shut down.
+ this.request = null;
+ this.itemsToStore = "";
+ this.itemsToStoreIndex = 0;
+ this.itemsStored = 0;
+ this.storeItemsTimer = null;
+
+ if (aFeed.downloadCallback)
+ aFeed.downloadCallback.downloaded(aFeed, aCode);
+ },
+
+ // nsITimerCallback
+ notify: function(aTimer)
+ {
+ this.storeNextItem();
+ }
+};
diff --git a/mailnews/extensions/newsblog/content/FeedItem.js b/mailnews/extensions/newsblog/content/FeedItem.js
new file mode 100644
index 000000000..09e4eb861
--- /dev/null
+++ b/mailnews/extensions/newsblog/content/FeedItem.js
@@ -0,0 +1,490 @@
+/* -*- 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/. */
+
+function FeedItem()
+{
+ this.mDate = FeedUtils.getValidRFC5322Date();
+ this.mUnicodeConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ this.mParserUtils = Cc["@mozilla.org/parserutils;1"].
+ getService(Ci.nsIParserUtils);
+}
+
+FeedItem.prototype =
+{
+ // Only for IETF Atom.
+ xmlContentBase: null,
+ id: null,
+ feed: null,
+ description: null,
+ content: null,
+ enclosures: [],
+ title: null,
+ author: "anonymous",
+ inReplyTo: "",
+ keywords: [],
+ mURL: null,
+ characterSet: "UTF-8",
+
+ ENCLOSURE_BOUNDARY_PREFIX: "--------------", // 14 dashes
+ ENCLOSURE_HEADER_BOUNDARY_PREFIX: "------------", // 12 dashes
+ MESSAGE_TEMPLATE: '\n' +
+ '<html>\n' +
+ ' <head>\n' +
+ ' <title>%TITLE%</title>\n' +
+ ' <base href="%BASE%">\n' +
+ ' </head>\n' +
+ ' <body id="msgFeedSummaryBody" selected="false">\n' +
+ ' %CONTENT%\n' +
+ ' </body>\n' +
+ '</html>\n',
+
+ get url()
+ {
+ return this.mURL;
+ },
+
+ set url(aVal)
+ {
+ try
+ {
+ this.mURL = Services.io.newURI(aVal, null, null).spec;
+ }
+ catch(ex)
+ {
+ // The url as published or constructed can be a non url. It's used as a
+ // feeditem identifier in feeditems.rdf, as a messageId, and as an href
+ // and for the content-base header. Save as is; ensure not null.
+ this.mURL = aVal ? aVal : "";
+ }
+ },
+
+ get date()
+ {
+ return this.mDate;
+ },
+
+ set date (aVal)
+ {
+ this.mDate = aVal;
+ },
+
+ get identity ()
+ {
+ return this.feed.name + ": " + this.title + " (" + this.id + ")"
+ },
+
+ normalizeMessageID: function(messageID)
+ {
+ // Escape occurrences of message ID meta characters <, >, and @.
+ messageID.replace(/</g, "%3C");
+ messageID.replace(/>/g, "%3E");
+ messageID.replace(/@/g, "%40");
+ messageID = "<" + messageID.trim() + "@" + "localhost.localdomain" + ">";
+
+ FeedUtils.log.trace("FeedItem.normalizeMessageID: messageID - " + messageID);
+ return messageID;
+ },
+
+ get itemUniqueURI()
+ {
+ return this.createURN(this.id);
+ },
+
+ get contentBase()
+ {
+ if(this.xmlContentBase)
+ return this.xmlContentBase
+ else
+ return this.mURL;
+ },
+
+ store: function()
+ {
+ // this.title and this.content contain HTML.
+ // this.mUrl and this.contentBase contain plain text.
+
+ let stored = false;
+ let resource = this.findStoredResource();
+ if (!this.feed.folder)
+ return stored;
+
+ if (resource == null)
+ {
+ resource = FeedUtils.rdf.GetResource(this.itemUniqueURI);
+ if (!this.content)
+ {
+ FeedUtils.log.trace("FeedItem.store: " + this.identity +
+ " no content; storing description or title");
+ this.content = this.description || this.title;
+ }
+
+ let content = this.MESSAGE_TEMPLATE;
+ content = content.replace(/%TITLE%/, this.title);
+ content = content.replace(/%BASE%/, this.htmlEscape(this.contentBase));
+ content = content.replace(/%CONTENT%/, this.content);
+ this.content = content;
+ this.writeToFolder();
+ this.markStored(resource);
+ stored = true;
+ }
+ this.markValid(resource);
+ return stored;
+ },
+
+ findStoredResource: function()
+ {
+ // Checks to see if the item has already been stored in its feed's
+ // message folder.
+ FeedUtils.log.trace("FeedItem.findStoredResource: checking if stored - " +
+ this.identity);
+
+ let server = this.feed.server;
+ let folder = this.feed.folder;
+
+ if (!folder)
+ {
+ FeedUtils.log.debug("FeedItem.findStoredResource: folder '" +
+ this.feed.folderName +
+ "' doesn't exist; creating as child of " +
+ server.rootMsgFolder.prettyName + "\n");
+ this.feed.createFolder();
+ return null;
+ }
+
+ let ds = FeedUtils.getItemsDS(server);
+ let itemURI = this.itemUniqueURI;
+ let itemResource = FeedUtils.rdf.GetResource(itemURI);
+
+ let downloaded = ds.GetTarget(itemResource, FeedUtils.FZ_STORED, true);
+
+ if (!downloaded ||
+ downloaded.QueryInterface(Ci.nsIRDFLiteral).Value == "false")
+ {
+ FeedUtils.log.trace("FeedItem.findStoredResource: not stored");
+ return null;
+ }
+
+ FeedUtils.log.trace("FeedItem.findStoredResource: already stored");
+ return itemResource;
+ },
+
+ markValid: function(resource)
+ {
+ let ds = FeedUtils.getItemsDS(this.feed.server);
+
+ let newTimeStamp = FeedUtils.rdf.GetLiteral(new Date().getTime());
+ let currentTimeStamp = ds.GetTarget(resource,
+ FeedUtils.FZ_LAST_SEEN_TIMESTAMP,
+ true);
+ if (currentTimeStamp)
+ ds.Change(resource, FeedUtils.FZ_LAST_SEEN_TIMESTAMP,
+ currentTimeStamp, newTimeStamp);
+ else
+ ds.Assert(resource, FeedUtils.FZ_LAST_SEEN_TIMESTAMP,
+ newTimeStamp, true);
+
+ if (!ds.HasAssertion(resource, FeedUtils.FZ_FEED,
+ FeedUtils.rdf.GetResource(this.feed.url), true))
+ ds.Assert(resource, FeedUtils.FZ_FEED,
+ FeedUtils.rdf.GetResource(this.feed.url), true);
+
+ if (ds.hasArcOut(resource, FeedUtils.FZ_VALID))
+ {
+ let currentValue = ds.GetTarget(resource, FeedUtils.FZ_VALID, true);
+ ds.Change(resource, FeedUtils.FZ_VALID,
+ currentValue, FeedUtils.RDF_LITERAL_TRUE);
+ }
+ else
+ ds.Assert(resource, FeedUtils.FZ_VALID, FeedUtils.RDF_LITERAL_TRUE, true);
+ },
+
+ markStored: function(resource)
+ {
+ let ds = FeedUtils.getItemsDS(this.feed.server);
+
+ if (!ds.HasAssertion(resource, FeedUtils.FZ_FEED,
+ FeedUtils.rdf.GetResource(this.feed.url), true))
+ ds.Assert(resource, FeedUtils.FZ_FEED,
+ FeedUtils.rdf.GetResource(this.feed.url), true);
+
+ let currentValue;
+ if (ds.hasArcOut(resource, FeedUtils.FZ_STORED))
+ {
+ currentValue = ds.GetTarget(resource, FeedUtils.FZ_STORED, true);
+ ds.Change(resource, FeedUtils.FZ_STORED,
+ currentValue, FeedUtils.RDF_LITERAL_TRUE);
+ }
+ else
+ ds.Assert(resource, FeedUtils.FZ_STORED,
+ FeedUtils.RDF_LITERAL_TRUE, true);
+ },
+
+ mimeEncodeSubject: function(aSubject, aCharset)
+ {
+ // This routine sometimes throws exceptions for mis-encoded data so
+ // wrap it with a try catch for now.
+ let newSubject;
+ try
+ {
+ newSubject = mailServices.mimeConverter.encodeMimePartIIStr_UTF8(aSubject,
+ false,
+ aCharset, 9, 72);
+ }
+ catch (ex)
+ {
+ newSubject = aSubject;
+ }
+
+ return newSubject;
+ },
+
+ writeToFolder: function()
+ {
+ FeedUtils.log.trace("FeedItem.writeToFolder: " + this.identity +
+ " writing to message folder " + this.feed.name);
+ // Convert the title to UTF-16 before performing our HTML entity
+ // replacement reg expressions.
+ let title = this.title;
+
+ // The subject may contain HTML entities. Convert these to their unencoded
+ // state. i.e. &amp; becomes '&'.
+ title = this.mParserUtils.convertToPlainText(
+ title,
+ Ci.nsIDocumentEncoder.OutputSelectionOnly |
+ Ci.nsIDocumentEncoder.OutputAbsoluteLinks,
+ 0);
+
+ // Compress white space in the subject to make it look better. Trim
+ // leading/trailing spaces to prevent mbox header folding issue at just
+ // the right subject length.
+ title = title.replace(/[\t\r\n]+/g, " ").trim();
+
+ this.title = this.mimeEncodeSubject(title, this.characterSet);
+
+ // If the date looks like it's in W3C-DTF format, convert it into
+ // an IETF standard date. Otherwise assume it's in IETF format.
+ if (this.mDate.search(/^\d\d\d\d/) != -1)
+ this.mDate = new Date(this.mDate).toUTCString();
+
+ // If there is an inreplyto value, create the headers.
+ let inreplytoHdrsStr = this.inReplyTo ?
+ ("References: " + this.inReplyTo + "\n" +
+ "In-Reply-To: " + this.inReplyTo + "\n") : "";
+
+ // If there are keywords (categories), create the headers. In the case of
+ // a longer than RFC5322 recommended line length, create multiple folded
+ // lines (easier to parse than multiple Keywords headers).
+ let keywordsStr = "";
+ if (this.keywords.length)
+ {
+ let HEADER = "Keywords: ";
+ let MAXLEN = 78;
+ keywordsStr = HEADER;
+ let keyword;
+ let keywords = [].concat(this.keywords);
+ let lines = [];
+ while (keywords.length)
+ {
+ keyword = keywords.shift();
+ if (keywordsStr.length + keyword.length > MAXLEN)
+ {
+ lines.push(keywordsStr)
+ keywordsStr = " ".repeat(HEADER.length);
+ }
+ keywordsStr += keyword + ",";
+ }
+ keywordsStr = keywordsStr.replace(/,$/,"\n");
+ lines.push(keywordsStr)
+ keywordsStr = lines.join("\n");
+ }
+
+ // Escape occurrences of "From " at the beginning of lines of
+ // content per the mbox standard, since "From " denotes a new
+ // message, and add a line break so we know the last line has one.
+ this.content = this.content.replace(/([\r\n]+)(>*From )/g, "$1>$2");
+ this.content += "\n";
+
+ // The opening line of the message, mandated by standards to start
+ // with "From ". It's useful to construct this separately because
+ // we not only need to write it into the message, we also need to
+ // use it to calculate the offset of the X-Mozilla-Status lines from
+ // the front of the message for the statusOffset property of the
+ // DB header object.
+ let openingLine = 'From - ' + this.mDate + '\n';
+
+ let source =
+ openingLine +
+ 'X-Mozilla-Status: 0000\n' +
+ 'X-Mozilla-Status2: 00000000\n' +
+ 'X-Mozilla-Keys: ' + " ".repeat(80) + '\n' +
+ 'Received: by localhost; ' + FeedUtils.getValidRFC5322Date() + '\n' +
+ 'Date: ' + this.mDate + '\n' +
+ 'Message-Id: ' + this.normalizeMessageID(this.id) + '\n' +
+ 'From: ' + this.author + '\n' +
+ 'MIME-Version: 1.0\n' +
+ 'Subject: ' + this.title + '\n' +
+ inreplytoHdrsStr +
+ keywordsStr +
+ 'Content-Transfer-Encoding: 8bit\n' +
+ 'Content-Base: ' + this.mURL + '\n';
+
+ if (this.enclosures.length)
+ {
+ let boundaryID = source.length;
+ source += 'Content-Type: multipart/mixed; boundary="' +
+ this.ENCLOSURE_HEADER_BOUNDARY_PREFIX + boundaryID + '"' + '\n\n' +
+ 'This is a multi-part message in MIME format.\n' +
+ this.ENCLOSURE_BOUNDARY_PREFIX + boundaryID + '\n' +
+ 'Content-Type: text/html; charset=' + this.characterSet + '\n' +
+ 'Content-Transfer-Encoding: 8bit\n' +
+ this.content;
+
+ this.enclosures.forEach(function(enclosure) {
+ source += enclosure.convertToAttachment(boundaryID);
+ });
+
+ source += this.ENCLOSURE_BOUNDARY_PREFIX + boundaryID + '--' + '\n\n\n';
+ }
+ else
+ source += 'Content-Type: text/html; charset=' + this.characterSet + '\n' +
+ this.content;
+
+ FeedUtils.log.trace("FeedItem.writeToFolder: " + this.identity +
+ " is " + source.length + " characters long");
+
+ // Get the folder and database storing the feed's messages and headers.
+ let folder = this.feed.folder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ let msgFolder = folder.QueryInterface(Ci.nsIMsgFolder);
+ msgFolder.gettingNewMessages = true;
+ // Source is a unicode string, we want to save a char * string in
+ // the original charset. So convert back.
+ this.mUnicodeConverter.charset = this.characterSet;
+ let msgDBHdr = folder.addMessage(this.mUnicodeConverter.ConvertFromUnicode(source));
+ msgDBHdr.OrFlags(Ci.nsMsgMessageFlags.FeedMsg);
+ msgFolder.gettingNewMessages = false;
+ this.tagItem(msgDBHdr, this.keywords);
+ },
+
+/**
+ * Autotag messages.
+ *
+ * @param nsIMsgDBHdr aMsgDBHdr - message to tag
+ * @param array aKeywords - keywords (tags)
+ */
+ tagItem: function(aMsgDBHdr, aKeywords)
+ {
+ let categoryPrefs = this.feed.categoryPrefs();
+ if (!aKeywords.length || !categoryPrefs.enabled)
+ return;
+
+ let msgArray = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+ msgArray.appendElement(aMsgDBHdr, false);
+
+ let prefix = categoryPrefs.prefixEnabled ? categoryPrefs.prefix : "";
+ let rtl = Services.prefs.getIntPref("bidi.direction") == 2;
+
+ let keys = [];
+ for (let keyword of aKeywords)
+ {
+ keyword = rtl ? keyword + prefix : prefix + keyword;
+ let keyForTag = MailServices.tags.getKeyForTag(keyword);
+ if (!keyForTag)
+ {
+ // Add the tag if it doesn't exist.
+ MailServices.tags.addTag(keyword, "", FeedUtils.AUTOTAG);
+ keyForTag = MailServices.tags.getKeyForTag(keyword);
+ }
+
+ // Add the tag key to the keys array.
+ keys.push(keyForTag);
+ }
+
+ if (keys.length)
+ // Add the keys to the message.
+ aMsgDBHdr.folder.addKeywordsToMessages(msgArray, keys.join(" "));
+ },
+
+ htmlEscape: function(s)
+ {
+ s = s.replace(/&/g, "&amp;");
+ s = s.replace(/>/g, "&gt;");
+ s = s.replace(/</g, "&lt;");
+ s = s.replace(/'/g, "&#39;");
+ s = s.replace(/"/g, "&quot;");
+ return s;
+ },
+
+ createURN: function(aName)
+ {
+ // Returns name as a URN in the 'feeditem' namespace. The returned URN is
+ // (or is intended to be) RFC2141 compliant.
+ // The builtin encodeURI provides nearly the exact encoding functionality
+ // required by the RFC. The exceptions are that NULL characters should not
+ // appear, and that #, /, ?, &, and ~ should be escaped.
+ // NULL characters are removed before encoding.
+
+ let name = aName.replace(/\0/g, "");
+ let encoded = encodeURI(name);
+ encoded = encoded.replace(/\#/g, "%23");
+ encoded = encoded.replace(/\//g, "%2f");
+ encoded = encoded.replace(/\?/g, "%3f");
+ encoded = encoded.replace(/\&/g, "%26");
+ encoded = encoded.replace(/\~/g, "%7e");
+
+ return FeedUtils.FZ_ITEM_NS + encoded;
+ }
+};
+
+
+// A feed enclosure is to RSS what an attachment is for e-mail. We make
+// enclosures look like attachments in the UI.
+function FeedEnclosure(aURL, aContentType, aLength, aTitle)
+{
+ this.mURL = aURL;
+ // Store a reasonable mimetype if content-type is not present.
+ this.mContentType = aContentType || "application/unknown";
+ this.mLength = aLength;
+ this.mTitle = aTitle;
+
+ // Generate a fileName from the URL.
+ if (this.mURL)
+ {
+ try
+ {
+ this.mFileName = Services.io.newURI(this.mURL, null, null).
+ QueryInterface(Ci.nsIURL).
+ fileName;
+ }
+ catch(ex)
+ {
+ this.mFileName = this.mURL;
+ }
+ }
+}
+
+FeedEnclosure.prototype =
+{
+ mURL: "",
+ mContentType: "",
+ mLength: 0,
+ mFileName: "",
+ mTitle: "",
+ ENCLOSURE_BOUNDARY_PREFIX: "--------------", // 14 dashes
+
+ // Returns a string that looks like an e-mail attachment which represents
+ // the enclosure.
+ convertToAttachment: function(aBoundaryID)
+ {
+ return '\n' +
+ this.ENCLOSURE_BOUNDARY_PREFIX + aBoundaryID + '\n' +
+ 'Content-Type: ' + this.mContentType +
+ '; name="' + (this.mTitle || this.mFileName) +
+ (this.mLength ? '"; size=' + this.mLength : '"') + '\n' +
+ 'X-Mozilla-External-Attachment-URL: ' + this.mURL + '\n' +
+ 'Content-Disposition: attachment; filename="' + this.mFileName + '"\n\n' +
+ FeedUtils.strings.GetStringFromName("externalAttachmentMsg") + '\n';
+ }
+};
diff --git a/mailnews/extensions/newsblog/content/FeedUtils.jsm b/mailnews/extensions/newsblog/content/FeedUtils.jsm
new file mode 100644
index 000000000..6d5e64dd2
--- /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);
+});
diff --git a/mailnews/extensions/newsblog/content/am-newsblog.js b/mailnews/extensions/newsblog/content/am-newsblog.js
new file mode 100644
index 000000000..674280f81
--- /dev/null
+++ b/mailnews/extensions/newsblog/content/am-newsblog.js
@@ -0,0 +1,63 @@
+/* -*- 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/. */
+
+Components.utils.import("resource:///modules/FeedUtils.jsm");
+
+var gServer, autotagEnable, autotagUsePrefix, autotagPrefix;
+
+function onInit(aPageId, aServerId)
+{
+ var accountName = document.getElementById("server.prettyName");
+ var title = document.getElementById("am-newsblog-title");
+ var defaultTitle = title.getAttribute("defaultTitle");
+
+ var titleValue;
+ if (accountName.value)
+ titleValue = defaultTitle + " - <" + accountName.value + ">";
+ else
+ titleValue = defaultTitle;
+
+ title.setAttribute("title", titleValue);
+ document.title = titleValue;
+
+ onCheckItem("server.biffMinutes", ["server.doBiff"]);
+
+ autotagEnable = document.getElementById("autotagEnable");
+ autotagUsePrefix = document.getElementById("autotagUsePrefix");
+ autotagPrefix = document.getElementById("autotagPrefix");
+
+ let categoryPrefsAcct = FeedUtils.getOptionsAcct(gServer).category;
+ autotagEnable.checked = categoryPrefsAcct.enabled;
+ autotagUsePrefix.disabled = !autotagEnable.checked;
+ autotagUsePrefix.checked = categoryPrefsAcct.prefixEnabled;
+ autotagPrefix.disabled = autotagUsePrefix.disabled || !autotagUsePrefix.checked;
+ autotagPrefix.value = categoryPrefsAcct.prefix;
+}
+
+function onPreInit(account, accountValues)
+{
+ gServer = account.incomingServer;
+}
+
+function setCategoryPrefs(aNode)
+{
+ let options = FeedUtils.getOptionsAcct(gServer);
+ switch (aNode.id) {
+ case "autotagEnable":
+ options.category.enabled = aNode.checked;
+ autotagUsePrefix.disabled = !aNode.checked;
+ autotagPrefix.disabled = !aNode.checked || !autotagUsePrefix.checked;
+ break;
+ case "autotagUsePrefix":
+ options.category.prefixEnabled = aNode.checked;
+ autotagPrefix.disabled = aNode.disabled || !aNode.checked;
+ break;
+ case "autotagPrefix":
+ options.category.prefix = aNode.value;
+ break;
+ }
+
+ FeedUtils.setOptionsAcct(gServer, options)
+}
diff --git a/mailnews/extensions/newsblog/content/am-newsblog.xul b/mailnews/extensions/newsblog/content/am-newsblog.xul
new file mode 100644
index 000000000..19173d803
--- /dev/null
+++ b/mailnews/extensions/newsblog/content/am-newsblog.xul
@@ -0,0 +1,155 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger-newsblog/skin/feed-subscriptions.css" type="text/css"?>
+
+<!DOCTYPE page [
+<!ENTITY % newsblogDTD SYSTEM "chrome://messenger-newsblog/locale/am-newsblog.dtd" >
+%newsblogDTD;
+<!ENTITY % feedDTD SYSTEM "chrome://messenger-newsblog/locale/feed-subscriptions.dtd" >
+%feedDTD;
+<!ENTITY % accountNoIdentDTD SYSTEM "chrome://messenger/locale/am-serverwithnoidentities.dtd" >
+%accountNoIdentDTD;
+<!ENTITY % accountServerTopDTD SYSTEM "chrome://messenger/locale/am-server-top.dtd">
+%accountServerTopDTD;
+]>
+
+<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ class="color-dialog"
+ title="&accountTitle.label;"
+ onload="parent.onPanelLoaded('am-newsblog.xul');">
+
+ <script type="application/javascript"
+ src="chrome://messenger/content/AccountManager.js"/>
+ <script type="application/javascript"
+ src="chrome://messenger-newsblog/content/am-newsblog.js"/>
+ <script type="application/javascript"
+ src="chrome://messenger-newsblog/content/newsblogOverlay.js"/>
+ <script type="application/javascript"
+ src="chrome://messenger/content/amUtils.js"/>
+ <script type="application/javascript"
+ src="chrome://messenger/content/am-prefs.js"/>
+
+ <vbox flex="1" style="overflow: auto;">
+
+ <dialogheader id="am-newsblog-title" defaultTitle="&accountTitle.label;"/>
+
+ <description class="secDesc">&accountSettingsDesc.label;</description>
+
+ <hbox align="center">
+ <label value="&accountName.label;"
+ accesskey="&accountName.accesskey;"
+ control="server.prettyName"/>
+ <textbox id="server.prettyName"
+ wsm_persist="true"
+ size="30"
+ prefstring="mail.server.%serverkey%.name"/>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <groupbox>
+ <caption label="&serverSettings.label;"/>
+
+ <checkbox id="server.loginAtStartUp"
+ wsm_persist="true"
+ label="&loginAtStartup.label;"
+ accesskey="&loginAtStartup.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.login_at_startup"/>
+
+ <hbox align="center">
+ <checkbox id="server.doBiff"
+ wsm_persist="true"
+ label="&biffStart.label;"
+ accesskey="&biffStart.accesskey;"
+ oncommand="onCheckItem('server.biffMinutes', [this.id]);"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.check_new_mail"/>
+ <textbox id="server.biffMinutes"
+ wsm_persist="true"
+ type="number"
+ size="3"
+ min="1"
+ increment="1"
+ preftype="int"
+ prefstring="mail.server.%serverkey%.check_time"
+ aria-labelledby="server.doBiff server.biffMinutes biffEnd"/>
+ <label id="biffEnd"
+ value="&biffEnd.label;"
+ control="server.biffMinutes"/>
+ </hbox>
+
+ <checkbox id="server.quickMode"
+ wsm_persist="true"
+ genericattr="true"
+ label="&useQuickMode.label;"
+ accesskey="&useQuickMode.accesskey;"
+ preftype="bool"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.quickMode"/>
+
+ <checkbox id="autotagEnable"
+ accesskey="&autotagEnable.accesskey;"
+ label="&autotagEnable.label;"
+ oncommand="setCategoryPrefs(this)"/>
+ <hbox>
+ <checkbox id="autotagUsePrefix"
+ class="indent"
+ accesskey="&autotagUsePrefix.accesskey;"
+ label="&autotagUsePrefix.label;"
+ oncommand="setCategoryPrefs(this)"/>
+ <textbox id="autotagPrefix"
+ placeholder="&autoTagPrefix.placeholder;"
+ clickSelectsAll="true"
+ onchange="setCategoryPrefs(this)"/>
+ </hbox>
+ </groupbox>
+
+ <separator class="thin"/>
+
+ <groupbox>
+ <caption label="&messageStorage.label;"/>
+
+ <checkbox id="server.emptyTrashOnExit"
+ wsm_persist="true"
+ label="&emptyTrashOnExit.label;"
+ accesskey="&emptyTrashOnExit.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.empty_trash_on_exit"/>
+
+ <separator class="thin"/>
+
+ <vbox>
+ <label value="&localPath.label;" control="server.localPath"/>
+ <hbox align="center">
+ <textbox readonly="true"
+ wsm_persist="true"
+ flex="1"
+ id="server.localPath"
+ datatype="nsIFile"
+ prefstring="mail.server.%serverkey%.directory"
+ class="uri-element"/>
+ <button id="browseForLocalFolder"
+ label="&browseFolder.label;"
+ filepickertitle="&localFolderPicker.label;"
+ accesskey="&browseFolder.accesskey;"
+ oncommand="BrowseForLocalFolders();"/>
+ </hbox>
+ </vbox>
+
+ </groupbox>
+
+ <separator class="thin"/>
+
+ <hbox align="center">
+ <spacer flex="1"/>
+ <button label="&manageSubscriptions.label;"
+ accesskey="&manageSubscriptions.accesskey;"
+ oncommand="openSubscriptionsDialog(gServer.rootFolder);"/>
+ </hbox>
+ </vbox>
+</page>
diff --git a/mailnews/extensions/newsblog/content/feed-parser.js b/mailnews/extensions/newsblog/content/feed-parser.js
new file mode 100644
index 000000000..660333422
--- /dev/null
+++ b/mailnews/extensions/newsblog/content/feed-parser.js
@@ -0,0 +1,1034 @@
+/* -*- 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/. */
+
+// The feed parser depends on FeedItem.js, Feed.js.
+function FeedParser() {
+ this.mSerializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
+ createInstance(Ci.nsIDOMSerializer);
+}
+
+FeedParser.prototype =
+{
+ // parseFeed() returns an array of parsed items ready for processing. It is
+ // currently a synchronous operation. If there is an error parsing the feed,
+ // parseFeed returns an empty feed in addition to calling aFeed.onParseError.
+ parseFeed: function (aFeed, aDOM)
+ {
+ if (!(aDOM instanceof Ci.nsIDOMXMLDocument))
+ {
+ // No xml doc.
+ return aFeed.onParseError(aFeed);
+ }
+
+ let doc = aDOM.documentElement;
+ if (doc.namespaceURI == FeedUtils.MOZ_PARSERERROR_NS)
+ {
+ // Gecko caught a basic parsing error.
+ let errStr = doc.firstChild.textContent + "\n" +
+ doc.firstElementChild.textContent;
+ FeedUtils.log.info("FeedParser.parseFeed: - " + errStr);
+ return aFeed.onParseError(aFeed);
+ }
+ else if (aDOM.querySelector("redirect"))
+ {
+ // Check for RSS2.0 redirect document.
+ let channel = aDOM.querySelector("redirect");
+ if (this.isPermanentRedirect(aFeed, channel, null, null))
+ return;
+
+ return aFeed.onParseError(aFeed);
+ }
+ else if (doc.namespaceURI == FeedUtils.RDF_SYNTAX_NS &&
+ doc.getElementsByTagNameNS(FeedUtils.RSS_NS, "channel")[0])
+ {
+ aFeed.mFeedType = "RSS_1.xRDF"
+ FeedUtils.log.debug("FeedParser.parseFeed: type:url - " +
+ aFeed.mFeedType +" : " +aFeed.url);
+ // aSource can be misencoded (XMLHttpRequest converts to UTF-8 by default),
+ // but the DOM is almost always right because it uses the hints in the
+ // XML file. This is slower, but not noticably so. Mozilla doesn't have
+ // the XMLHttpRequest.responseBody property that IE has, which provides
+ // access to the unencoded response.
+ let xmlString = this.mSerializer.serializeToString(doc);
+ return this.parseAsRSS1(aFeed, xmlString, aFeed.request.channel.URI);
+ }
+ else if (doc.namespaceURI == FeedUtils.ATOM_03_NS)
+ {
+ aFeed.mFeedType = "ATOM_0.3"
+ FeedUtils.log.debug("FeedParser.parseFeed: type:url - " +
+ aFeed.mFeedType +" : " +aFeed.url);
+ return this.parseAsAtom(aFeed, aDOM);
+ }
+ else if (doc.namespaceURI == FeedUtils.ATOM_IETF_NS)
+ {
+ aFeed.mFeedType = "ATOM_IETF"
+ FeedUtils.log.debug("FeedParser.parseFeed: type:url - " +
+ aFeed.mFeedType +" : " +aFeed.url);
+ return this.parseAsAtomIETF(aFeed, aDOM);
+ }
+ else if (doc.getElementsByTagNameNS(FeedUtils.RSS_090_NS, "channel")[0])
+ {
+ aFeed.mFeedType = "RSS_0.90"
+ FeedUtils.log.debug("FeedParser.parseFeed: type:url - " +
+ aFeed.mFeedType +" : " +aFeed.url);
+ return this.parseAsRSS2(aFeed, aDOM);
+ }
+ else
+ {
+ // Parse as RSS 0.9x. In theory even RSS 1.0 feeds could be parsed by
+ // the 0.9x parser if the RSS namespace were the default.
+ let rssVer = doc.localName == "rss" ? doc.getAttribute("version") : null;
+ if (rssVer)
+ aFeed.mFeedType = "RSS_" + rssVer;
+ else
+ aFeed.mFeedType = "RSS_0.9x?";
+ FeedUtils.log.debug("FeedParser.parseFeed: type:url - " +
+ aFeed.mFeedType +" : " +aFeed.url);
+ return this.parseAsRSS2(aFeed, aDOM);
+ }
+ },
+
+ parseAsRSS2: function (aFeed, aDOM)
+ {
+ // Get the first channel (assuming there is only one per RSS File).
+ let parsedItems = new Array();
+
+ let channel = aDOM.querySelector("channel");
+ if (!channel)
+ return aFeed.onParseError(aFeed);
+
+ // Usually the empty string, unless this is RSS .90.
+ let nsURI = channel.namespaceURI || "";
+ FeedUtils.log.debug("FeedParser.parseAsRSS2: channel nsURI - " + nsURI);
+
+ if (this.isPermanentRedirect(aFeed, null, channel, null))
+ return;
+
+ let tags = this.childrenByTagNameNS(channel, nsURI, "title");
+ aFeed.title = aFeed.title || this.getNodeValue(tags ? tags[0] : null);
+ tags = this.childrenByTagNameNS(channel, nsURI, "description");
+ aFeed.description = this.getNodeValueFormatted(tags ? tags[0] : null);
+ tags = this.childrenByTagNameNS(channel, nsURI, "link");
+ aFeed.link = this.validLink(this.getNodeValue(tags ? tags[0] : null));
+
+ if (!(aFeed.title || aFeed.description) || !aFeed.link)
+ {
+ FeedUtils.log.error("FeedParser.parseAsRSS2: missing mandatory element " +
+ "<title> and <description>, or <link>");
+ return aFeed.onParseError(aFeed);
+ }
+
+ if (!aFeed.parseItems)
+ return parsedItems;
+
+ aFeed.invalidateItems();
+ // XXX use getElementsByTagNameNS for now; childrenByTagNameNS would be
+ // better, but RSS .90 is still with us.
+ let itemNodes = aDOM.getElementsByTagNameNS(nsURI, "item");
+ itemNodes = itemNodes ? itemNodes : [];
+ FeedUtils.log.debug("FeedParser.parseAsRSS2: items to parse - " +
+ itemNodes.length);
+
+ for (let itemNode of itemNodes)
+ {
+ if (!itemNode.childElementCount)
+ continue;
+ let item = new FeedItem();
+ item.feed = aFeed;
+ item.enclosures = [];
+ item.keywords = [];
+
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.FEEDBURNER_NS, "origLink");
+ let link = this.validLink(this.getNodeValue(tags ? tags[0] : null));
+ if (!link)
+ {
+ tags = this.childrenByTagNameNS(itemNode, nsURI, "link");
+ link = this.validLink(this.getNodeValue(tags ? tags[0] : null));
+ }
+ tags = this.childrenByTagNameNS(itemNode, nsURI, "guid");
+ let guidNode = tags ? tags[0] : null;
+
+ let guid;
+ let isPermaLink = false;
+ if (guidNode)
+ {
+ guid = this.getNodeValue(guidNode);
+ // isPermaLink is true if the value is "true" or if the attribute is
+ // not present; all other values, including "false" and "False" and
+ // for that matter "TRuE" and "meatcake" are false.
+ if (!guidNode.hasAttribute("isPermaLink") ||
+ guidNode.getAttribute("isPermaLink") == "true")
+ isPermaLink = true;
+ // If attribute isPermaLink is missing, it is good to check the validity
+ // of <guid> value as an URL to avoid linking to non-URL strings.
+ if (!guidNode.hasAttribute("isPermaLink"))
+ {
+ try
+ {
+ Services.io.newURI(guid, null, null);
+ if (Services.io.extractScheme(guid) == "tag")
+ isPermaLink = false;
+ }
+ catch (ex)
+ {
+ isPermaLink = false;
+ }
+ }
+
+ item.id = guid;
+ }
+
+ let guidLink = this.validLink(guid);
+ item.url = isPermaLink && guidLink ? guidLink : link ? link : null;
+ tags = this.childrenByTagNameNS(itemNode, nsURI, "description");
+ item.description = this.getNodeValueFormatted(tags ? tags[0] : null);
+ tags = this.childrenByTagNameNS(itemNode, nsURI, "title");
+ item.title = this.getNodeValue(tags ? tags[0] : null);
+ if (!(item.title || item.description))
+ {
+ FeedUtils.log.info("FeedParser.parseAsRSS2: <item> missing mandatory " +
+ "element, either <title> or <description>; skipping");
+ continue;
+ }
+
+ if (!item.id)
+ {
+ // At this point, if there is no guid, uniqueness cannot be guaranteed
+ // by any of link or date (optional) or title (optional unless there
+ // is no description). Use a big chunk of description; minimize dupes
+ // with url and title if present.
+ item.id = (item.url || item.feed.url) + "#" + item.title + "#" +
+ (this.stripTags(item.description ?
+ item.description.substr(0, 150) : null) ||
+ item.title);
+ item.id = item.id.replace(/[\n\r\t\s]+/g, " ");
+ }
+
+ // Escape html entities in <title>, which are unescaped as textContent
+ // values. If the title is used as content, it will remain escaped; if
+ // it is used as the title, it will be unescaped upon store. Bug 1240603.
+ // The <description> tag must follow escaping examples found in
+ // http://www.rssboard.org/rss-encoding-examples, i.e. single escape angle
+ // brackets for tags, which are removed if used as title, and double
+ // escape entities for presentation in title.
+ // Better: always use <title>. Best: use Atom.
+ if (!item.title)
+ item.title = this.stripTags(item.description).substr(0, 150);
+ else
+ item.title = item.htmlEscape(item.title);
+
+ tags = this.childrenByTagNameNS(itemNode, nsURI, "author");
+ if (!tags)
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.DC_NS, "creator");
+ item.author = this.getNodeValue(tags ? tags[0] : null) ||
+ aFeed.title ||
+ item.author;
+
+ tags = this.childrenByTagNameNS(itemNode, nsURI, "pubDate");
+ if (!tags || !this.getNodeValue(tags[0]))
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.DC_NS, "date");
+ item.date = this.getNodeValue(tags ? tags[0] : null) || item.date;
+
+ // If the date is invalid, users will see the beginning of the epoch
+ // unless we reset it here, so they'll see the current time instead.
+ // This is typical aggregator behavior.
+ if (item.date)
+ {
+ item.date = item.date.trim();
+ if (!FeedUtils.isValidRFC822Date(item.date))
+ {
+ // XXX Use this on the other formats as well.
+ item.date = this.dateRescue(item.date);
+ }
+ }
+
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.RSS_CONTENT_NS, "encoded");
+ item.content = this.getNodeValueFormatted(tags ? tags[0] : null);
+
+ // Handle <enclosures> and <media:content>, which may be in a
+ // <media:group> (if present).
+ tags = this.childrenByTagNameNS(itemNode, nsURI, "enclosure");
+ let encUrls = [];
+ if (tags)
+ for (let tag of tags)
+ {
+ let url = this.validLink(tag.getAttribute("url"));
+ if (url && encUrls.indexOf(url) == -1)
+ {
+ let type = this.removeUnprintableASCII(tag.getAttribute("type"));
+ let length = this.removeUnprintableASCII(tag.getAttribute("length"));
+ item.enclosures.push(new FeedEnclosure(url, type, length));
+ encUrls.push(url);
+ }
+ }
+
+ tags = itemNode.getElementsByTagNameNS(FeedUtils.MRSS_NS, "content");
+ if (tags)
+ for (let tag of tags)
+ {
+ let url = this.validLink(tag.getAttribute("url"));
+ if (url && encUrls.indexOf(url) == -1)
+ {
+ let type = this.removeUnprintableASCII(tag.getAttribute("type"));
+ let fileSize = this.removeUnprintableASCII(tag.getAttribute("fileSize"));
+ item.enclosures.push(new FeedEnclosure(url, type, fileSize));
+ }
+ }
+
+ // The <origEnclosureLink> tag has no specification, especially regarding
+ // whether more than one tag is allowed and, if so, how tags would
+ // relate to previously declared (and well specified) enclosure urls.
+ // The common usage is to include 1 origEnclosureLink, in addition to
+ // the specified enclosure tags for 1 enclosure. Thus, we will replace the
+ // first enclosure's, if found, url with the first <origEnclosureLink>
+ // url only or else add the <origEnclosureLink> url.
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.FEEDBURNER_NS, "origEnclosureLink");
+ let origEncUrl = this.validLink(this.getNodeValue(tags ? tags[0] : null));
+ if (origEncUrl)
+ {
+ if (item.enclosures.length)
+ item.enclosures[0].mURL = origEncUrl;
+ else
+ item.enclosures.push(new FeedEnclosure(origEncUrl));
+ }
+
+ // Support <category> and autotagging.
+ tags = this.childrenByTagNameNS(itemNode, nsURI, "category");
+ if (tags)
+ {
+ for (let tag of tags)
+ {
+ let term = this.getNodeValue(tag);
+ term = term ? this.xmlUnescape(term.replace(/,/g, ";")) : null;
+ if (term && item.keywords.indexOf(term) == -1)
+ item.keywords.push(term);
+ }
+ }
+
+ parsedItems.push(item);
+ }
+
+ return parsedItems;
+ },
+
+ parseAsRSS1 : function(aFeed, aSource, aBaseURI)
+ {
+ let parsedItems = new Array();
+
+ // RSS 1.0 is valid RDF, so use the RDF parser/service to extract data.
+ // Create a new RDF data source and parse the feed into it.
+ let ds = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"].
+ createInstance(Ci.nsIRDFDataSource);
+
+ let rdfparser = Cc["@mozilla.org/rdf/xml-parser;1"].
+ createInstance(Ci.nsIRDFXMLParser);
+ rdfparser.parseString(ds, aBaseURI, aSource);
+
+ // Get information about the feed as a whole.
+ let channel = ds.GetSource(FeedUtils.RDF_TYPE, FeedUtils.RSS_CHANNEL, true);
+ if (!channel)
+ return aFeed.onParseError(aFeed);
+
+ if (this.isPermanentRedirect(aFeed, null, channel, ds))
+ return;
+
+ aFeed.title = aFeed.title ||
+ this.getRDFTargetValue(ds, channel, FeedUtils.RSS_TITLE) ||
+ aFeed.url;
+ aFeed.description = this.getRDFTargetValueFormatted(ds, channel, FeedUtils.RSS_DESCRIPTION) ||
+ "";
+ aFeed.link = this.validLink(this.getRDFTargetValue(ds, channel, FeedUtils.RSS_LINK)) ||
+ aFeed.url;
+
+ if (!(aFeed.title || aFeed.description) || !aFeed.link)
+ {
+ FeedUtils.log.error("FeedParser.parseAsRSS1: missing mandatory element " +
+ "<title> and <description>, or <link>");
+ return aFeed.onParseError(aFeed);
+ }
+
+ if (!aFeed.parseItems)
+ return parsedItems;
+
+ aFeed.invalidateItems();
+
+ // Ignore the <items> list and just get the <item>s.
+ let items = ds.GetSources(FeedUtils.RDF_TYPE, FeedUtils.RSS_ITEM, true);
+
+ let index = 0;
+ while (items.hasMoreElements())
+ {
+ let itemResource = items.getNext().QueryInterface(Ci.nsIRDFResource);
+ let item = new FeedItem();
+ item.feed = aFeed;
+
+ // Prefer the value of the link tag to the item URI since the URI could be
+ // a relative URN.
+ let uri = itemResource.ValueUTF8;
+ let link = this.validLink(this.getRDFTargetValue(ds, itemResource, FeedUtils.RSS_LINK));
+ item.url = link || uri;
+ item.description = this.getRDFTargetValueFormatted(ds, itemResource,
+ FeedUtils.RSS_DESCRIPTION);
+ item.title = this.getRDFTargetValue(ds, itemResource, FeedUtils.RSS_TITLE) ||
+ this.getRDFTargetValue(ds, itemResource, FeedUtils.DC_SUBJECT) ||
+ (item.description ?
+ (this.stripTags(item.description).substr(0, 150)) : null);
+ if (!item.url || !item.title)
+ {
+ FeedUtils.log.info("FeedParser.parseAsRSS1: <item> missing mandatory " +
+ "element <item rdf:about> and <link>, or <title> and " +
+ "no <description>; skipping");
+ continue;
+ }
+
+ item.id = item.url;
+ item.url = this.validLink(item.url);
+
+ item.author = this.getRDFTargetValue(ds, itemResource, FeedUtils.DC_CREATOR) ||
+ this.getRDFTargetValue(ds, channel, FeedUtils.DC_CREATOR) ||
+ aFeed.title ||
+ item.author;
+ item.date = this.getRDFTargetValue(ds, itemResource, FeedUtils.DC_DATE) ||
+ item.date;
+ item.content = this.getRDFTargetValueFormatted(ds, itemResource,
+ FeedUtils.RSS_CONTENT_ENCODED);
+
+ parsedItems[index++] = item;
+ }
+ FeedUtils.log.debug("FeedParser.parseAsRSS1: items parsed - " + index);
+
+ return parsedItems;
+ },
+
+ parseAsAtom: function(aFeed, aDOM)
+ {
+ let parsedItems = new Array();
+
+ // Get the first channel (assuming there is only one per Atom File).
+ let channel = aDOM.querySelector("feed");
+ if (!channel)
+ return aFeed.onParseError(aFeed);
+
+ if (this.isPermanentRedirect(aFeed, null, channel, null))
+ return;
+
+ let tags = this.childrenByTagNameNS(channel, FeedUtils.ATOM_03_NS, "title");
+ aFeed.title = aFeed.title ||
+ this.stripTags(this.getNodeValue(tags ? tags[0] : null));
+ tags = this.childrenByTagNameNS(channel, FeedUtils.ATOM_03_NS, "tagline");
+ aFeed.description = this.getNodeValueFormatted(tags ? tags[0] : null);
+ tags = this.childrenByTagNameNS(channel, FeedUtils.ATOM_03_NS, "link");
+ aFeed.link = this.validLink(this.findAtomLink("alternate", tags));
+
+ if (!aFeed.title)
+ {
+ FeedUtils.log.error("FeedParser.parseAsAtom: missing mandatory element " +
+ "<title>");
+ return aFeed.onParseError(aFeed);
+ }
+
+ if (!aFeed.parseItems)
+ return parsedItems;
+
+ aFeed.invalidateItems();
+ let items = this.childrenByTagNameNS(channel, FeedUtils.ATOM_03_NS, "entry");
+ items = items ? items : [];
+ FeedUtils.log.debug("FeedParser.parseAsAtom: items to parse - " +
+ items.length);
+
+ for (let itemNode of items)
+ {
+ if (!itemNode.childElementCount)
+ continue;
+ let item = new FeedItem();
+ item.feed = aFeed;
+
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_03_NS, "link");
+ item.url = this.validLink(this.findAtomLink("alternate", tags));
+
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_03_NS, "id");
+ item.id = this.getNodeValue(tags ? tags[0] : null);
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_03_NS, "summary");
+ item.description = this.getNodeValueFormatted(tags ? tags[0] : null);
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_03_NS, "title");
+ item.title = this.getNodeValue(tags ? tags[0] : null) ||
+ (item.description ? item.description.substr(0, 150) : null);
+ if (!item.title || !item.id)
+ {
+ // We're lenient about other mandatory tags, but insist on these.
+ FeedUtils.log.info("FeedParser.parseAsAtom: <entry> missing mandatory " +
+ "element <id>, or <title> and no <summary>; skipping");
+ continue;
+ }
+
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_03_NS, "author");
+ if (!tags)
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_03_NS, "contributor");
+ if (!tags)
+ tags = this.childrenByTagNameNS(channel, FeedUtils.ATOM_03_NS, "author");
+
+ let authorEl = tags ? tags[0] : null;
+
+ let author = "";
+ if (authorEl)
+ {
+ tags = this.childrenByTagNameNS(authorEl, FeedUtils.ATOM_03_NS, "name");
+ let name = this.getNodeValue(tags ? tags[0] : null);
+ tags = this.childrenByTagNameNS(authorEl, FeedUtils.ATOM_03_NS, "email");
+ let email = this.getNodeValue(tags ? tags[0] : null);
+ if (name)
+ author = name + (email ? " <" + email + ">" : "");
+ else if (email)
+ author = email;
+ }
+
+ item.author = author || item.author || aFeed.title;
+
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_03_NS, "modified");
+ if (!tags || !this.getNodeValue(tags[0]))
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_03_NS, "issued");
+ if (!tags || !this.getNodeValue(tags[0]))
+ tags = this.childrenByTagNameNS(channel, FeedUtils.ATOM_03_NS, "created");
+
+ item.date = this.getNodeValue(tags ? tags[0] : null) || item.date;
+
+ // XXX We should get the xml:base attribute from the content tag as well
+ // and use it as the base HREF of the message.
+ // XXX Atom feeds can have multiple content elements; we should differentiate
+ // between them and pick the best one.
+ // Some Atom feeds wrap the content in a CTYPE declaration; others use
+ // a namespace to identify the tags as HTML; and a few are buggy and put
+ // HTML tags in without declaring their namespace so they look like Atom.
+ // We deal with the first two but not the third.
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_03_NS, "content");
+ let contentNode = tags ? tags[0] : null;
+
+ let content;
+ if (contentNode)
+ {
+ content = "";
+ for (let j = 0; j < contentNode.childNodes.length; j++)
+ {
+ let node = contentNode.childNodes.item(j);
+ if (node.nodeType == node.CDATA_SECTION_NODE)
+ content += node.data;
+ else
+ content += this.mSerializer.serializeToString(node);
+ }
+
+ if (contentNode.getAttribute("mode") == "escaped")
+ {
+ content = content.replace(/&lt;/g, "<");
+ content = content.replace(/&gt;/g, ">");
+ content = content.replace(/&amp;/g, "&");
+ }
+
+ if (content == "")
+ content = null;
+ }
+
+ item.content = content;
+ parsedItems.push(item);
+ }
+
+ return parsedItems;
+ },
+
+ parseAsAtomIETF: function(aFeed, aDOM)
+ {
+ let parsedItems = new Array();
+
+ // Get the first channel (assuming there is only one per Atom File).
+ let channel = this.childrenByTagNameNS(aDOM, FeedUtils.ATOM_IETF_NS, "feed")[0];
+ if (!channel)
+ return aFeed.onParseError(aFeed);
+
+ if (this.isPermanentRedirect(aFeed, null, channel, null))
+ return;
+
+ let tags = this.childrenByTagNameNS(channel, FeedUtils.ATOM_IETF_NS, "title");
+ aFeed.title = aFeed.title ||
+ this.stripTags(this.serializeTextConstruct(tags ? tags[0] : null));
+
+ tags = this.childrenByTagNameNS(channel, FeedUtils.ATOM_IETF_NS, "subtitle");
+ aFeed.description = this.serializeTextConstruct(tags ? tags[0] : null);
+
+ tags = this.childrenByTagNameNS(channel, FeedUtils.ATOM_IETF_NS, "link");
+ aFeed.link = this.findAtomLink("alternate", tags);
+ aFeed.link = this.validLink(aFeed.link);
+
+ if (!aFeed.title)
+ {
+ FeedUtils.log.error("FeedParser.parseAsAtomIETF: missing mandatory element " +
+ "<title>");
+ return aFeed.onParseError(aFeed);
+ }
+
+ if (!aFeed.parseItems)
+ return parsedItems;
+
+ aFeed.invalidateItems();
+ let items = this.childrenByTagNameNS(channel, FeedUtils.ATOM_IETF_NS, "entry");
+ items = items ? items : [];
+ FeedUtils.log.debug("FeedParser.parseAsAtomIETF: items to parse - " +
+ items.length);
+
+ for (let itemNode of items)
+ {
+ if (!itemNode.childElementCount)
+ continue;
+ let item = new FeedItem();
+ item.feed = aFeed;
+ item.enclosures = [];
+ item.keywords = [];
+
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.FEEDBURNER_NS, "origLink");
+ item.url = this.validLink(this.getNodeValue(tags ? tags[0] : null));
+ if (!item.url)
+ {
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_IETF_NS, "link");
+ item.url = this.validLink(this.findAtomLink("alternate", tags)) ||
+ aFeed.link;
+ }
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_IETF_NS, "id");
+ item.id = this.getNodeValue(tags ? tags[0] : null);
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_IETF_NS, "summary");
+ item.description = this.serializeTextConstruct(tags ? tags[0] : null);
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_IETF_NS, "title");
+ item.title = this.stripTags(this.serializeTextConstruct(tags ? tags[0] : null) ||
+ (item.description ?
+ item.description.substr(0, 150) : null));
+ if (!item.title || !item.id)
+ {
+ // We're lenient about other mandatory tags, but insist on these.
+ FeedUtils.log.info("FeedParser.parseAsAtomIETF: <entry> missing mandatory " +
+ "element <id>, or <title> and no <summary>; skipping");
+ continue;
+ }
+
+ // XXX Support multiple authors.
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_IETF_NS, "source");
+ let source = tags ? tags[0] : null;
+
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_IETF_NS, "author");
+ if (!tags)
+ tags = this.childrenByTagNameNS(source, FeedUtils.ATOM_IETF_NS, "author");
+ if (!tags)
+ tags = this.childrenByTagNameNS(channel, FeedUtils.ATOM_IETF_NS, "author");
+
+ let authorEl = tags ? tags[0] : null;
+
+ let author = "";
+ if (authorEl)
+ {
+ tags = this.childrenByTagNameNS(authorEl, FeedUtils.ATOM_IETF_NS, "name");
+ let name = this.getNodeValue(tags ? tags[0] : null);
+ tags = this.childrenByTagNameNS(authorEl, FeedUtils.ATOM_IETF_NS, "email");
+ let email = this.getNodeValue(tags ? tags[0] : null);
+ if (name)
+ author = name + (email ? " <" + email + ">" : "");
+ else if (email)
+ author = email;
+ }
+
+ item.author = author || item.author || aFeed.title;
+
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_IETF_NS, "updated");
+ if (!tags || !this.getNodeValue(tags[0]))
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_IETF_NS, "published");
+ if (!tags || !this.getNodeValue(tags[0]))
+ tags = this.childrenByTagNameNS(source, FeedUtils.ATOM_IETF_NS, "published");
+ item.date = this.getNodeValue(tags ? tags[0] : null) || item.date;
+
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_IETF_NS, "content");
+ item.content = this.serializeTextConstruct(tags ? tags[0] : null);
+
+ if (item.content)
+ item.xmlContentBase = tags ? tags[0].baseURI : null;
+ else if (item.description)
+ {
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_IETF_NS, "summary");
+ item.xmlContentBase = tags ? tags[0].baseURI : null;
+ }
+ else
+ item.xmlContentBase = itemNode.baseURI;
+
+ item.xmlContentBase = this.validLink(item.xmlContentBase);
+
+ // Handle <link rel="enclosure"> (if present).
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_IETF_NS, "link");
+ let encUrls = [];
+ if (tags)
+ for (let tag of tags)
+ {
+ let url = tag.getAttribute("rel") == "enclosure" ?
+ (tag.getAttribute("href") || "").trim() : null;
+ url = this.validLink(url);
+ if (url && encUrls.indexOf(url) == -1)
+ {
+ let type = this.removeUnprintableASCII(tag.getAttribute("type"));
+ let length = this.removeUnprintableASCII(tag.getAttribute("length"));
+ let title = this.removeUnprintableASCII(tag.getAttribute("title"));
+ item.enclosures.push(new FeedEnclosure(url, type, length, title));
+ encUrls.push(url);
+ }
+ }
+
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.FEEDBURNER_NS, "origEnclosureLink");
+ let origEncUrl = this.validLink(this.getNodeValue(tags ? tags[0] : null));
+ if (origEncUrl)
+ {
+ if (item.enclosures.length)
+ item.enclosures[0].mURL = origEncUrl;
+ else
+ item.enclosures.push(new FeedEnclosure(origEncUrl));
+ }
+
+ // Handle atom threading extension, RFC4685. There may be 1 or more tags,
+ // and each must contain a ref attribute with 1 Message-Id equivalent
+ // value. This is the only attr of interest in the spec for presentation.
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_THREAD_NS, "in-reply-to");
+ if (tags)
+ {
+ for (let tag of tags)
+ {
+ let ref = this.removeUnprintableASCII(tag.getAttribute("ref"));
+ if (ref)
+ item.inReplyTo += item.normalizeMessageID(ref) + " ";
+ }
+ item.inReplyTo = item.inReplyTo.trimRight();
+ }
+
+ // Support <category> and autotagging.
+ tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_IETF_NS, "category");
+ if (tags)
+ {
+ for (let tag of tags)
+ {
+ let term = this.removeUnprintableASCII(tag.getAttribute("term"));
+ term = term ? this.xmlUnescape(term.replace(/,/g, ";")).trim() : null;
+ if (term && item.keywords.indexOf(term) == -1)
+ item.keywords.push(term);
+ }
+ }
+
+ parsedItems.push(item);
+ }
+
+ return parsedItems;
+ },
+
+ isPermanentRedirect: function(aFeed, aRedirDocChannel, aFeedChannel, aDS)
+ {
+ // If subscribing to a new feed, do not check redirect tags.
+ if (!aFeed.downloadCallback || aFeed.downloadCallback.mSubscribeMode)
+ return false;
+
+ let tags, tagName, newUrl;
+ let oldUrl = aFeed.url;
+
+ // Check for RSS2.0 redirect document <newLocation> tag.
+ if (aRedirDocChannel)
+ {
+ tagName = "newLocation";
+ tags = this.childrenByTagNameNS(aRedirDocChannel, "", tagName);
+ newUrl = this.getNodeValue(tags ? tags[0] : null);
+ }
+
+ // Check for <itunes:new-feed-url> tag.
+ if (aFeedChannel)
+ {
+ tagName = "new-feed-url";
+ if (aDS)
+ {
+ tags = FeedUtils.rdf.GetResource(FeedUtils.ITUNES_NS + tagName);
+ newUrl = this.getRDFTargetValue(aDS, aFeedChannel, tags);
+ }
+ else
+ {
+ tags = this.childrenByTagNameNS(aFeedChannel, FeedUtils.ITUNES_NS, tagName);
+ newUrl = this.getNodeValue(tags ? tags[0] : null);
+ }
+ tagName = "itunes:" + tagName;
+ }
+
+ if (newUrl && newUrl != oldUrl && FeedUtils.isValidScheme(newUrl) &&
+ FeedUtils.changeUrlForFeed(aFeed, newUrl))
+ {
+ FeedUtils.log.info("FeedParser.isPermanentRedirect: found <" + tagName +
+ "> tag; updated feed url from: " + oldUrl + " to: " + newUrl +
+ " in folder: " + FeedUtils.getFolderPrettyPath(aFeed.folder));
+ aFeed.onUrlChange(aFeed, oldUrl);
+ return true;
+ }
+
+ return false;
+ },
+
+ serializeTextConstruct: function(textElement)
+ {
+ let content = "";
+ if (textElement)
+ {
+ let textType = textElement.getAttribute("type");
+
+ // Atom spec says consider it "text" if not present.
+ if (!textType)
+ textType = "text";
+
+ // There could be some strange content type we don't handle.
+ if (textType != "text" && textType != "html" && textType != "xhtml")
+ return null;
+
+ for (let j = 0; j < textElement.childNodes.length; j++)
+ {
+ let node = textElement.childNodes.item(j);
+ if (node.nodeType == node.CDATA_SECTION_NODE)
+ content += this.xmlEscape(node.data);
+ else
+ content += this.mSerializer.serializeToString(node);
+ }
+
+ if (textType == "html")
+ content = this.xmlUnescape(content);
+
+ content = content.trim();
+ }
+
+ // Other parts of the code depend on this being null if there's no content.
+ return content ? content : null;
+ },
+
+ getRDFTargetValue: function(ds, source, property)
+ {
+ let nodeValue = this.getRDFTargetValueRaw(ds, source, property);
+ if (!nodeValue)
+ return null;
+
+ nodeValue = nodeValue.replace(/[\n\r\t]+/g, " ");
+ return this.removeUnprintableASCII(nodeValue);
+
+ },
+
+ getRDFTargetValueFormatted: function(ds, source, property)
+ {
+ let nodeValue = this.getRDFTargetValueRaw(ds, source, property);
+ if (!nodeValue)
+ return null;
+
+ return this.removeUnprintableASCIIexCRLFTAB(nodeValue);
+
+ },
+
+ getRDFTargetValueRaw: function(ds, source, property)
+ {
+ let node = ds.GetTarget(source, property, true);
+ if (node)
+ {
+ try
+ {
+ node = node.QueryInterface(Ci.nsIRDFLiteral);
+ if (node)
+ return node.Value.trim();
+ }
+ catch (e)
+ {
+ // If the RDF was bogus, do nothing. Rethrow if it's some other problem.
+ if (!((e instanceof Ci.nsIXPCException) &&
+ e.result == Cr.NS_ERROR_NO_INTERFACE))
+ throw new Error("FeedParser.getRDFTargetValue: " + e);
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Return a cleaned up node value. This is intended for values that are not
+ * multiline and not formatted. A sequence of tab or newline is converted to
+ * a space and unprintable ascii is removed.
+ *
+ * @param {Node} node - A DOM node.
+ * @return {String} - A clean string value or null.
+ */
+ getNodeValue: function(node)
+ {
+ let nodeValue = this.getNodeValueRaw(node);
+ if (!nodeValue)
+ return null;
+
+ nodeValue = nodeValue.replace(/[\n\r\t]+/g, " ");
+ return this.removeUnprintableASCII(nodeValue);
+ },
+
+ /**
+ * Return a cleaned up formatted node value, meaning CR/LF/TAB are retained
+ * while all other unprintable ascii is removed. This is intended for values
+ * that are multiline and formatted, such as content or description tags.
+ *
+ * @param {Node} node - A DOM node.
+ * @return {String} - A clean string value or null.
+ */
+ getNodeValueFormatted: function(node)
+ {
+ let nodeValue = this.getNodeValueRaw(node);
+ if (!nodeValue)
+ return null;
+
+ return this.removeUnprintableASCIIexCRLFTAB(nodeValue);
+ },
+
+ /**
+ * Return a raw node value, as received. This should be sanitized as
+ * appropriate.
+ *
+ * @param {Node} node - A DOM node.
+ * @return {String} - A string value or null.
+ */
+ getNodeValueRaw: function(node)
+ {
+ if (node && node.textContent)
+ return node.textContent.trim();
+
+ if (node && node.firstChild)
+ {
+ let ret = "";
+ for (let child = node.firstChild; child; child = child.nextSibling)
+ {
+ let value = this.getNodeValueRaw(child);
+ if (value)
+ ret += value;
+ }
+
+ if (ret)
+ return ret.trim();
+ }
+
+ return null;
+ },
+
+ // Finds elements that are direct children of the first arg.
+ childrenByTagNameNS: function(aElement, aNamespace, aTagName)
+ {
+ if (!aElement)
+ return null;
+
+ let matches = aElement.getElementsByTagNameNS(aNamespace, aTagName);
+ let matchingChildren = new Array();
+ for (let match of matches)
+ {
+ if (match.parentNode == aElement)
+ matchingChildren.push(match)
+ }
+
+ return matchingChildren.length ? matchingChildren : null;
+ },
+
+ /**
+ * Ensure <link> type tags start with http[s]://, ftp:// or magnet:
+ * for values stored in mail headers (content-base and remote enclosures),
+ * particularly to prevent data: uris, javascript, and other spoofing.
+ *
+ * @param {String} link - An intended http url string.
+ * @return {String} - A clean string starting with http, ftp or magnet,
+ * else null.
+ */
+ validLink: function(link)
+ {
+ if (/^((https?|ftp):\/\/|magnet:)/.test(link))
+ return this.removeUnprintableASCII(link.trim());
+
+ return null;
+ },
+
+ findAtomLink: function(linkRel, linkElements)
+ {
+ if (!linkElements)
+ return null;
+
+ // XXX Need to check for MIME type and hreflang.
+ for (let alink of linkElements) {
+ if (alink &&
+ // If there's a link rel.
+ ((alink.getAttribute("rel") && alink.getAttribute("rel") == linkRel) ||
+ // If there isn't, assume 'alternate'.
+ (!alink.getAttribute("rel") && (linkRel == "alternate"))) &&
+ alink.getAttribute("href"))
+ {
+ // Atom links are interpreted relative to xml:base.
+ try {
+ return Services.io.newURI(alink.baseURI, null, null).
+ resolve(alink.getAttribute("href"));
+ }
+ catch (ex) {}
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Remove unprintable ascii, particularly CR/LF, for non formatted tag values.
+ *
+ * @param {String} s - String to clean.
+ * @return {String}
+ */
+ removeUnprintableASCII: function(s)
+ {
+ return s ? s.replace(/[\x00-\x1F\x7F]+/g, "") : "";
+ },
+
+ /**
+ * Remove unprintable ascii, except CR/LF/TAB, for formatted tag values.
+ *
+ * @param {String} s - String to clean.
+ * @return {String}
+ */
+ removeUnprintableASCIIexCRLFTAB: function(s)
+ {
+ return s ? s.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]+/g, "") : "";
+ },
+
+ stripTags: function(someHTML)
+ {
+ return someHTML ? someHTML.replace(/<[^>]+>/g, "") : someHTML;
+ },
+
+ xmlUnescape: function(s)
+ {
+ s = s.replace(/&lt;/g, "<");
+ s = s.replace(/&gt;/g, ">");
+ s = s.replace(/&amp;/g, "&");
+ return s;
+ },
+
+ xmlEscape: function(s)
+ {
+ s = s.replace(/&/g, "&amp;");
+ s = s.replace(/>/g, "&gt;");
+ s = s.replace(/</g, "&lt;");
+ return s;
+ },
+
+ dateRescue: function(dateString)
+ {
+ // Deal with various kinds of invalid dates.
+ if (!isNaN(parseInt(dateString)))
+ {
+ // It's an integer, so maybe it's a timestamp.
+ let d = new Date(parseInt(dateString) * 1000);
+ let now = new Date();
+ let yeardiff = now.getFullYear() - d.getFullYear();
+ FeedUtils.log.trace("FeedParser.dateRescue: Rescue Timestamp date - " +
+ d.toString() + " ,year diff - " + yeardiff);
+ if (yeardiff >= 0 && yeardiff < 3)
+ // It's quite likely the correct date.
+ return d.toString();
+ }
+
+ // Could be an ISO8601/W3C date. If not, get the current time.
+ return FeedUtils.getValidRFC5322Date(dateString);
+ }
+};
diff --git a/mailnews/extensions/newsblog/content/feed-subscriptions.js b/mailnews/extensions/newsblog/content/feed-subscriptions.js
new file mode 100644
index 000000000..2b77e60c4
--- /dev/null
+++ b/mailnews/extensions/newsblog/content/feed-subscriptions.js
@@ -0,0 +1,2703 @@
+/* -*- 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/. */
+
+Components.utils.import("resource:///modules/FeedUtils.jsm");
+Components.utils.import("resource:///modules/gloda/log4moz.js");
+Components.utils.import("resource:///modules/mailServices.js");
+Components.utils.import("resource://gre/modules/FileUtils.jsm");
+Components.utils.import("resource://gre/modules/PluralForm.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var {classes: Cc, interfaces: Ci} = Components;
+
+var FeedSubscriptions = {
+ get mMainWin() { return Services.wm.getMostRecentWindow("mail:3pane"); },
+
+ get mTree() { return document.getElementById("rssSubscriptionsList"); },
+
+ mFeedContainers: [],
+ mRSSServer : null,
+ mActionMode : null,
+ kSubscribeMode : 1,
+ kUpdateMode : 2,
+ kMoveMode : 3,
+ kCopyMode : 4,
+ kImportingOPML : 5,
+ kVerifyUrlMode : 6,
+
+ get FOLDER_ACTIONS()
+ {
+ return Ci.nsIMsgFolderNotificationService.folderAdded |
+ Ci.nsIMsgFolderNotificationService.folderDeleted |
+ Ci.nsIMsgFolderNotificationService.folderRenamed |
+ Ci.nsIMsgFolderNotificationService.folderMoveCopyCompleted;
+ },
+
+ onLoad: function ()
+ {
+ // Extract the folder argument.
+ let folder;
+ if (window.arguments && window.arguments[0].folder)
+ folder = window.arguments[0].folder;
+
+ // Ensure dialog is fully loaded before selecting, to get visible row.
+ setTimeout(function() {
+ FeedSubscriptions.refreshSubscriptionView(folder)
+ }, 100);
+ let message = FeedUtils.strings.GetStringFromName("subscribe-loading");
+ this.updateStatusItem("statusText", message);
+
+ FeedUtils.CANCEL_REQUESTED = false;
+
+ if (this.mMainWin)
+ {
+ this.mMainWin.FeedFolderNotificationService = MailServices.mfn;
+ this.mMainWin.FeedFolderNotificationService
+ .addListener(this.FolderListener, this.FOLDER_ACTIONS);
+ }
+ },
+
+ onClose: function ()
+ {
+ let dismissDialog = true;
+
+ // If we are in the middle of subscribing to a feed, inform the user that
+ // dismissing the dialog right now will abort the feed subscription.
+ if (this.mActionMode == this.kSubscribeMode)
+ {
+ let pTitle = FeedUtils.strings.GetStringFromName(
+ "subscribe-cancelSubscriptionTitle");
+ let pMessage = FeedUtils.strings.GetStringFromName(
+ "subscribe-cancelSubscription");
+ dismissDialog =
+ !(Services.prompt.confirmEx(window, pTitle, pMessage,
+ Ci.nsIPromptService.STD_YES_NO_BUTTONS,
+ null, null, null, null, { }));
+ }
+
+ if (dismissDialog)
+ {
+ FeedUtils.CANCEL_REQUESTED = this.mActionMode == this.kSubscribeMode;
+ if (this.mMainWin)
+ {
+ this.mMainWin.FeedFolderNotificationService
+ .removeListener(this.FolderListener, this.FOLDER_ACTIONS);
+ delete this.mMainWin.FeedFolderNotificationService;
+ }
+ }
+
+ return dismissDialog;
+ },
+
+ refreshSubscriptionView: function(aSelectFolder, aSelectFeedUrl)
+ {
+ let item = this.mView.currentItem;
+ this.loadSubscriptions();
+ this.mTree.view = this.mView;
+
+ if (aSelectFolder && !aSelectFeedUrl)
+ this.selectFolder(aSelectFolder);
+ else
+ {
+ // If no folder to select, try to select the pre rebuild selection, in
+ // an existing window. For folderpane changes in a feed account.
+ if (item)
+ {
+ let rootFolder = item.container ? item.folder.rootFolder :
+ item.parentFolder.rootFolder;
+ if (item.container)
+ {
+ if (!this.selectFolder(item.folder, { open: item.open }))
+ // The item no longer exists, an ancestor folder was deleted or
+ // renamed/moved.
+ this.selectFolder(rootFolder);
+ }
+ else {
+ let url = item.parentFolder == aSelectFolder ? aSelectFeedUrl :
+ item.url;
+ this.selectFeed({ folder: rootFolder, url: url }, null);
+ }
+ }
+ }
+
+ this.mView.treeBox.ensureRowIsVisible(this.mView.selection.currentIndex);
+ this.clearStatusInfo();
+ },
+
+ mView:
+ {
+ kRowIndexUndefined: -1,
+
+ get currentItem() {
+ // Get the current selection, if any.
+ let seln = this.selection;
+ let currentSelectionIndex = seln ? seln.currentIndex : null;
+ let item;
+ if (currentSelectionIndex != null)
+ item = this.getItemAtIndex(currentSelectionIndex);
+
+ return item;
+ },
+
+ /* nsITreeView */
+ treeBox: null,
+
+ mRowCount: 0,
+ get rowCount() { return this.mRowCount; },
+
+ _selection: null,
+ get selection () { return this._selection; },
+ set selection (val) { return this._selection = val; },
+
+ setTree: function(aTreebox) { this.treeBox = aTreebox; },
+ isSeparator: function(aRow) { return false; },
+ isSorted: function() { return false; },
+ isSelectable: function(aRow, aColumn) { return false; },
+ isEditable: function (aRow, aColumn) { return false; },
+
+ getProgressMode : function(aRow, aCol) {},
+ cycleHeader: function(aCol) {},
+ cycleCell: function(aRow, aCol) {},
+ selectionChanged: function() {},
+ performAction: function(aAction) {},
+ performActionOnRow: function (aAction, aRow) {},
+ performActionOnCell: function(aAction, aRow, aCol) {},
+ getRowProperties: function(aRow) { return ""; },
+ getColumnProperties: function(aCol) { return ""; },
+ getCellValue: function (aRow, aColumn) {},
+ setCellValue: function (aRow, aColumn, aValue) {},
+ setCellText: function (aRow, aColumn, aValue) {},
+
+ getCellProperties: function (aRow, aColumn) {
+ let item = this.getItemAtIndex(aRow);
+ let folder = item && item.folder ? item.folder : null;
+#ifdef MOZ_THUNDERBIRD
+ let properties = ["folderNameCol"];
+ let hasFeeds = folder ? FeedUtils.getFeedUrlsInFolder(folder) : false;
+ let prop = !folder ? "isFeed-true" :
+ hasFeeds ? "isFeedFolder-true" :
+ folder.isServer ? "serverType-rss isServer-true" : null;
+ if (prop)
+ properties.push(prop);
+ return properties.join(" ");
+#else
+ return !folder ? "serverType-rss" :
+ folder.isServer ? "serverType-rss isServer-true" : "livemark";
+#endif
+ },
+
+ isContainer: function (aRow)
+ {
+ let item = this.getItemAtIndex(aRow);
+ return item ? item.container : false;
+ },
+
+ isContainerOpen: function (aRow)
+ {
+ let item = this.getItemAtIndex(aRow);
+ return item ? item.open : false;
+ },
+
+ isContainerEmpty: function (aRow)
+ {
+ let item = this.getItemAtIndex(aRow);
+ if (!item)
+ return false;
+
+ return item.children.length == 0;
+ },
+
+ getItemAtIndex: function (aRow)
+ {
+ if (aRow < 0 || aRow >= FeedSubscriptions.mFeedContainers.length)
+ return null;
+
+ return FeedSubscriptions.mFeedContainers[aRow];
+ },
+
+ getItemInViewIndex: function(aFolder)
+ {
+ if (!aFolder || !(aFolder instanceof Ci.nsIMsgFolder))
+ return null;
+
+ for (let index = 0; index < this.rowCount; index++)
+ {
+ // Find the visible folder in the view.
+ let item = this.getItemAtIndex(index);
+ if (item && item.container && item.url == aFolder.URI)
+ return index;
+ }
+
+ return null;
+ },
+
+ removeItemAtIndex: function (aRow, aNoSelect)
+ {
+ let itemToRemove = this.getItemAtIndex(aRow);
+ if (!itemToRemove)
+ return;
+
+ if (itemToRemove.container && itemToRemove.open)
+ // Close it, if open container.
+ this.toggleOpenState(aRow);
+
+ let parentIndex = this.getParentIndex(aRow);
+ let hasNextSibling = this.hasNextSibling(aRow, aRow);
+ if (parentIndex != this.kRowIndexUndefined)
+ {
+ let parent = this.getItemAtIndex(parentIndex);
+ if (parent)
+ {
+ for (let index = 0; index < parent.children.length; index++)
+ if (parent.children[index] == itemToRemove)
+ {
+ parent.children.splice(index, 1);
+ break;
+ }
+ }
+ }
+
+ // Now remove it from our view.
+ FeedSubscriptions.mFeedContainers.splice(aRow, 1);
+
+ // Now invalidate the correct tree rows.
+ this.mRowCount--;
+ this.treeBox.rowCountChanged(aRow, -1);
+
+ // Now update the selection position, unless noSelect (selection is
+ // done later or not at all). If the item is the last child, select the
+ // parent. Otherwise select the next sibling.
+ if (!aNoSelect) {
+ if (aRow <= FeedSubscriptions.mFeedContainers.length)
+ this.selection.select(hasNextSibling ? aRow : aRow - 1);
+ else
+ this.selection.clearSelection();
+ }
+
+ // Now refocus the tree.
+ FeedSubscriptions.mTree.focus();
+ },
+
+ getCellText: function (aRow, aColumn)
+ {
+ let item = this.getItemAtIndex(aRow);
+ return (item && aColumn.id == "folderNameCol") ? item.name : "";
+ },
+
+ getImageSrc: function(aRow, aCol)
+ {
+ let item = this.getItemAtIndex(aRow);
+ if ((item.folder && item.folder.isServer) || item.open)
+ return "";
+
+ if (item.favicon != null)
+ return item.favicon;
+
+ if (item.folder && FeedSubscriptions.mMainWin &&
+ "gFolderTreeView" in FeedSubscriptions.mMainWin) {
+ let favicon = FeedSubscriptions.mMainWin.gFolderTreeView
+ .getFolderCacheProperty(item.folder, "favicon");
+ if (favicon != null)
+ return item.favicon = favicon;
+ }
+
+ let callback = (iconUrl => {
+ item.favicon = iconUrl;
+ if (item.folder)
+ {
+ for (let child of item.children)
+ if (!child.container)
+ {
+ child.favicon = iconUrl;
+ break;
+ }
+ }
+
+ this.selection.tree.invalidateRow(aRow);
+ });
+
+ // A closed non server folder.
+ if (item.folder)
+ {
+ for (let child of item.children)
+ {
+ if (!child.container) {
+ if (child.favicon != null)
+ return child.favicon;
+
+ setTimeout(() => {
+ FeedUtils.getFavicon(child.parentFolder, child.url, null,
+ window, callback);
+ }, 0);
+ break;
+ }
+ }
+ }
+ else
+ {
+ // A feed.
+ setTimeout(() => {
+ FeedUtils.getFavicon(item.parentFolder, item.url, null,
+ window, callback);
+ }, 0);
+ }
+
+ // Store empty string to return default while favicons are retrieved.
+ return item.favicon = "";
+ },
+
+ canDrop: function (aRow, aOrientation)
+ {
+ let dropResult = this.extractDragData(aRow);
+ return aOrientation == Ci.nsITreeView.DROP_ON && dropResult.canDrop &&
+ (dropResult.dropUrl || dropResult.dropOnIndex != this.kRowIndexUndefined);
+ },
+
+ drop: function (aRow, aOrientation)
+ {
+ let win = FeedSubscriptions;
+ let results = this.extractDragData(aRow);
+ if (!results.canDrop)
+ return;
+
+ // Preselect the drop folder.
+ this.selection.select(aRow);
+
+ if (results.dropUrl)
+ {
+ // Don't freeze the app that initiated the drop just because we are
+ // in a loop waiting for the user to dimisss the add feed dialog.
+ setTimeout(function() {
+ win.addFeed(results.dropUrl, null, true, null, win.kSubscribeMode);
+ }, 0);
+ let folderItem = this.getItemAtIndex(aRow);
+ FeedUtils.log.debug("drop: folder, url - " +
+ folderItem.folder.name + ", " + results.dropUrl);
+ }
+ else if (results.dropOnIndex != this.kRowIndexUndefined)
+ {
+ win.moveCopyFeed(results.dropOnIndex, aRow, results.dropEffect);
+ }
+ },
+
+ // Helper function for drag and drop.
+ extractDragData: function(aRow)
+ {
+ let dt = this._currentDataTransfer;
+ let dragDataResults = { canDrop: false,
+ dropUrl: null,
+ dropOnIndex: this.kRowIndexUndefined,
+ dropEffect: dt.dropEffect };
+
+ if (dt.getData("text/x-moz-feed-index"))
+ {
+ // Dragging a feed in the tree.
+ if (this.selection)
+ {
+ dragDataResults.dropOnIndex = this.selection.currentIndex;
+
+ let curItem = this.getItemAtIndex(this.selection.currentIndex);
+ let newItem = this.getItemAtIndex(aRow);
+ let curServer = curItem && curItem.parentFolder ?
+ curItem.parentFolder.server : null;
+ let newServer = newItem && newItem.folder ?
+ newItem.folder.server : null;
+
+ // No copying within the same account and no moving to the account
+ // folder in the same account.
+ if (!(curServer == newServer &&
+ (dragDataResults.dropEffect == "copy" ||
+ newItem.folder == curItem.parentFolder ||
+ newItem.folder.isServer)))
+ dragDataResults.canDrop = true;
+ }
+ }
+ else
+ {
+ // Try to get a feed url.
+ let validUri = FeedUtils.getFeedUriFromDataTransfer(dt);
+
+ if (validUri)
+ {
+ dragDataResults.canDrop = true;
+ dragDataResults.dropUrl = validUri.spec;
+ }
+ }
+
+ return dragDataResults;
+ },
+
+ getParentIndex: function (aRow)
+ {
+ let item = this.getItemAtIndex(aRow);
+
+ if (item)
+ {
+ for (let index = aRow; index >= 0; index--)
+ if (FeedSubscriptions.mFeedContainers[index].level < item.level)
+ return index;
+ }
+
+ return this.kRowIndexUndefined;
+ },
+
+ isIndexChildOfParentIndex: function (aRow, aChildRow)
+ {
+ // For visible tree rows, not if items are children of closed folders.
+ let item = this.getItemAtIndex(aRow);
+ if (!item || aChildRow <= aRow)
+ return false;
+
+ let targetLevel = this.getItemAtIndex(aRow).level;
+ let rows = FeedSubscriptions.mFeedContainers;
+
+ for (let i = aRow + 1; i < rows.length; i++) {
+ if (this.getItemAtIndex(i).level <= targetLevel)
+ break;
+ if (aChildRow == i)
+ return true;
+ }
+
+ return false;
+ },
+
+ hasNextSibling: function(aRow, aAfterIndex) {
+ let targetLevel = this.getItemAtIndex(aRow).level;
+ let rows = FeedSubscriptions.mFeedContainers;
+ for (let i = aAfterIndex + 1; i < rows.length; i++) {
+ if (this.getItemAtIndex(i).level == targetLevel)
+ return true;
+ if (this.getItemAtIndex(i).level < targetLevel)
+ return false;
+ }
+
+ return false;
+ },
+
+ hasPreviousSibling: function (aRow)
+ {
+ let item = this.getItemAtIndex(aRow);
+ if (item && aRow)
+ return this.getItemAtIndex(aRow - 1).level == item.level;
+ else
+ return false;
+ },
+
+ getLevel: function (aRow)
+ {
+ let item = this.getItemAtIndex(aRow);
+ if (!item)
+ return 0;
+
+ return item.level;
+ },
+
+ toggleOpenState: function (aRow)
+ {
+ let item = this.getItemAtIndex(aRow);
+ if (!item)
+ return;
+
+ // Save off the current selection item.
+ let seln = this.selection;
+ let currentSelectionIndex = seln.currentIndex;
+
+ let rowsChanged = this.toggle(aRow)
+
+ // Now restore selection, ensuring selection is maintained on toggles.
+ if (currentSelectionIndex > aRow)
+ seln.currentIndex = currentSelectionIndex + rowsChanged;
+ else
+ seln.select(currentSelectionIndex);
+
+ seln.selectEventsSuppressed = false;
+ },
+
+ toggle: function (aRow)
+ {
+ // Collapse the row, or build sub rows based on open states in the map.
+ let item = this.getItemAtIndex(aRow);
+ if (!item)
+ return null;
+
+ let rows = FeedSubscriptions.mFeedContainers;
+ let rowCount = 0;
+ let multiplier;
+
+ function addDescendants(aItem)
+ {
+ for (let i = 0; i < aItem.children.length; i++)
+ {
+ rowCount++;
+ let child = aItem.children[i];
+ rows.splice(aRow + rowCount, 0, child);
+ if (child.open)
+ addDescendants(child);
+ }
+ }
+
+ if (item.open)
+ {
+ // Close the container. Add up all subfolders and their descendants
+ // who may be open.
+ multiplier = -1;
+ let nextRow = aRow + 1;
+ let nextItem = rows[nextRow];
+ while (nextItem && nextItem.level > item.level)
+ {
+ rowCount++;
+ nextItem = rows[++nextRow];
+ }
+
+ rows.splice(aRow + 1, rowCount);
+ }
+ else
+ {
+ // Open the container. Restore the open state of all subfolder and
+ // their descendants.
+ multiplier = 1;
+ addDescendants(item);
+ }
+
+ let delta = multiplier * rowCount;
+ this.mRowCount += delta;
+
+ item.open = !item.open;
+ // Suppress the select event caused by rowCountChanged.
+ this.selection.selectEventsSuppressed = true;
+ // Add or remove the children from our view.
+ this.treeBox.rowCountChanged(aRow, delta);
+ return delta;
+ }
+ },
+
+ makeFolderObject: function (aFolder, aCurrentLevel)
+ {
+ let defaultQuickMode = aFolder.server.getBoolValue("quickMode");
+ let optionsAcct = aFolder.isServer ? FeedUtils.getOptionsAcct(aFolder.server) :
+ null;
+ let open = !aFolder.isServer &&
+ aFolder.server == this.mRSSServer &&
+ this.mActionMode == this.kImportingOPML ? true : false
+ let folderObject = { children : [],
+ folder : aFolder,
+ name : aFolder.prettyName,
+ level : aCurrentLevel,
+ url : aFolder.URI,
+ quickMode: defaultQuickMode,
+ options : optionsAcct,
+ open : open,
+ container: true,
+ favicon : null };
+
+ // If a feed has any sub folders, add them to the list of children.
+ let folderEnumerator = aFolder.subFolders;
+
+ while (folderEnumerator.hasMoreElements())
+ {
+ let folder = folderEnumerator.getNext();
+ if ((folder instanceof Ci.nsIMsgFolder) &&
+ !folder.getFlag(Ci.nsMsgFolderFlags.Trash) &&
+ !folder.getFlag(Ci.nsMsgFolderFlags.Virtual))
+ {
+ folderObject.children
+ .push(this.makeFolderObject(folder, aCurrentLevel + 1));
+ }
+ }
+
+ let feeds = this.getFeedsInFolder(aFolder);
+ for (let feed of feeds)
+ {
+ // Now add any feed urls for the folder.
+ folderObject.children.push(this.makeFeedObject(feed,
+ aFolder,
+ aCurrentLevel + 1));
+ }
+
+ // Finally, set the folder's quickMode based on the its first feed's
+ // quickMode, since that is how the view determines summary mode, and now
+ // quickMode is updated to be the same for all feeds in a folder.
+ if (feeds && feeds[0])
+ folderObject.quickMode = feeds[0].quickMode;
+
+ folderObject.children = this.folderItemSorter(folderObject.children);
+
+ return folderObject;
+ },
+
+ folderItemSorter: function (aArray)
+ {
+ return aArray.sort(function(a, b) { return a.name.toLowerCase() >
+ b.name.toLowerCase() }).
+ sort(function(a, b) { return a.container < b.container });
+ },
+
+ getFeedsInFolder: function (aFolder)
+ {
+ let feeds = new Array();
+ let feedUrlArray = FeedUtils.getFeedUrlsInFolder(aFolder);
+ if (!feedUrlArray)
+ // No feedUrls in this folder.
+ return feeds;
+
+ for (let url of feedUrlArray)
+ {
+ let feedResource = FeedUtils.rdf.GetResource(url);
+ let feed = new Feed(feedResource, aFolder.server);
+ feeds.push(feed);
+ }
+
+ return feeds;
+ },
+
+ makeFeedObject: function (aFeed, aFolder, aLevel)
+ {
+ // Look inside the data source for the feed properties.
+ let feed = { children : [],
+ parentFolder: aFolder,
+ name : aFeed.title || aFeed.description || aFeed.url,
+ url : aFeed.url,
+ quickMode : aFeed.quickMode,
+ options : aFeed.options || FeedUtils.optionsTemplate,
+ level : aLevel,
+ open : false,
+ container : false,
+ favicon : null };
+ return feed;
+ },
+
+ loadSubscriptions: function ()
+ {
+ // Put together an array of folders. Each feed account level folder is
+ // included as the root.
+ let numFolders = 0;
+ let feedContainers = [];
+ // Get all the feed account folders.
+ let feedRootFolders = FeedUtils.getAllRssServerRootFolders();
+
+ feedRootFolders.forEach(function(rootFolder) {
+ feedContainers.push(this.makeFolderObject(rootFolder, 0));
+ numFolders++;
+ }, this);
+
+ this.mFeedContainers = feedContainers;
+ this.mView.mRowCount = numFolders;
+
+ FeedSubscriptions.mTree.focus();
+ },
+
+ /**
+ * Find the folder in the tree. The search may be limited to subfolders of
+ * a known folder, or expanded to include the entire tree. This function is
+ * also used to insert/remove folders without rebuilding the tree view cache
+ * (to avoid position/toggle state loss).
+ *
+ * @param aFolder nsIMsgFolder - the folder to find.
+ * @param [aParams] object - params object, containing:
+ *
+ * [parentIndex] int - index of folder to start the search; if
+ * null (default), the index of the folder's
+ * rootFolder will be used.
+ * [select] boolean - if true (default) the folder's ancestors
+ * will be opened and the folder selected.
+ * [open] boolean - if true (default) the folder is opened.
+ * [remove] boolean - delete the item from tree row cache if true,
+ * false (default) otherwise.
+ * [newFolder] nsIMsgFolder - if not null (default) the new folder,
+ * for add or rename.
+ *
+ * @return bool found - true if found, false if not.
+ */
+ selectFolder: function(aFolder, aParms)
+ {
+ let folderURI = aFolder.URI;
+ let parentIndex = aParms && ("parentIndex" in aParms) ? aParms.parentIndex : null;
+ let selectIt = aParms && ("select" in aParms) ? aParms.select : true;
+ let openIt = aParms && ("open" in aParms) ? aParms.open : true;
+ let removeIt = aParms && ("remove" in aParms) ? aParms.remove : false;
+ let newFolder = aParms && ("newFolder" in aParms) ? aParms.newFolder : null;
+ let startIndex, startItem;
+ let found = false;
+
+ let firstVisRow, curFirstVisRow, curLastVisRow;
+ if (this.mView.treeBox)
+ firstVisRow = this.mView.treeBox.getFirstVisibleRow();
+
+ if (parentIndex != null)
+ {
+ // Use the parentIndex if given.
+ startIndex = parentIndex;
+ if (aFolder.isServer)
+ // Fake item for account root folder.
+ startItem = { name: "AccountRoot",
+ children: [this.mView.getItemAtIndex(startIndex)],
+ container: true, open: false, url: null, level: -1};
+ else
+ startItem = this.mView.getItemAtIndex(startIndex);
+ }
+ else
+ {
+ // Get the folder's root parent index.
+ let index = 0;
+ for (index; index < this.mView.rowCount; index++)
+ {
+ let item = this.mView.getItemAtIndex(index);
+ if (item.url == aFolder.server.rootFolder.URI)
+ break;
+ }
+ startIndex = index;
+ if (aFolder.isServer)
+ // Fake item for account root folder.
+ startItem = { name: "AccountRoot",
+ children: [this.mView.getItemAtIndex(startIndex)],
+ container: true, open: false, url: null, level: -1};
+ else
+ startItem = this.mView.getItemAtIndex(startIndex);
+ }
+
+ function containsFolder(aItem)
+ {
+ // Search for the folder. If it's found, set the open state on all
+ // ancestor folders. A toggle() rebuilds the view rows to match the map.
+ if (aItem.url == folderURI)
+ return found = true;
+
+ for (let i = 0; i < aItem.children.length; i++) {
+ if (aItem.children[i].container && containsFolder(aItem.children[i]))
+ {
+ if (removeIt && aItem.children[i].url == folderURI)
+ {
+ // Get all occurences in the tree cache arrays.
+ FeedUtils.log.debug("selectFolder: delete in cache, " +
+ "parent:children:item:index - "+
+ aItem.name + ":" + aItem.children.length + ":" +
+ aItem.children[i].name + ":" + i);
+ aItem.children.splice(i, 1);
+ FeedUtils.log.debug("selectFolder: deleted in cache, " +
+ "parent:children - " +
+ aItem.name + ":" + aItem.children.length);
+ removeIt = false;
+ return true;
+ }
+ if (newFolder)
+ {
+ let newItem = FeedSubscriptions.makeFolderObject(newFolder,
+ aItem.level + 1);
+ newItem.open = aItem.children[i].open;
+ if (newFolder.isServer)
+ FeedSubscriptions.mFeedContainers[startIndex] = newItem;
+ else
+ {
+ aItem.children[i] = newItem;
+ aItem.children = FeedSubscriptions.folderItemSorter(aItem.children);
+ }
+ FeedUtils.log.trace("selectFolder: parentName:newFolderName:newFolderItem - " +
+ aItem.name + ":" + newItem.name + ":" + newItem.toSource());
+ newFolder = null;
+ return true;
+ }
+ if (!found)
+ {
+ // For the folder to find.
+ found = true;
+ aItem.children[i].open = openIt;
+ }
+ else
+ {
+ // For ancestor folders.
+ if (selectIt || openIt)
+ aItem.children[i].open = true;
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ if (startItem)
+ {
+ // Find a folder with a specific parent.
+ containsFolder(startItem);
+ if (!found)
+ return false;
+
+ if (!selectIt)
+ return true;
+
+ if (startItem.open)
+ this.mView.toggle(startIndex);
+ this.mView.toggleOpenState(startIndex);
+ }
+
+ for (let index = 0; index < this.mView.rowCount && selectIt; index++)
+ {
+ // The desired folder is now in the view.
+ let item = this.mView.getItemAtIndex(index);
+ if (!item.container)
+ continue;
+ if (item.url == folderURI)
+ {
+ if (item.children.length &&
+ ((!item.open && openIt) || (item.open && !openIt)))
+ this.mView.toggleOpenState(index);
+ this.mView.selection.select(index);
+ found = true;
+ break;
+ }
+ }
+
+ // Ensure tree position does not jump unnecessarily.
+ curFirstVisRow = this.mView.treeBox.getFirstVisibleRow();
+ curLastVisRow = this.mView.treeBox.getLastVisibleRow();
+ if (firstVisRow >= 0 &&
+ this.mView.rowCount - curLastVisRow > firstVisRow - curFirstVisRow)
+ this.mView.treeBox.scrollToRow(firstVisRow);
+ else
+ this.mView.treeBox.ensureRowIsVisible(this.mView.rowCount - 1);
+
+ FeedUtils.log.debug("selectFolder: curIndex:firstVisRow:" +
+ "curFirstVisRow:curLastVisRow:rowCount - " +
+ this.mView.selection.currentIndex + ":" +
+ firstVisRow + ":" +
+ curFirstVisRow + ":" + curLastVisRow + ":" + this.mView.rowCount);
+
+ return found;
+ },
+
+ /**
+ * Find the feed in the tree. The search first gets the feed's folder,
+ * then selects the child feed.
+ *
+ * @param aFeed {Feed object} - the feed to find.
+ * @param [aParentIndex] integer - index to start the folder search.
+ *
+ * @return found bool - true if found, false if not.
+ */
+ selectFeed: function(aFeed, aParentIndex)
+ {
+ let folder = aFeed.folder;
+ let found = false;
+
+ if (aFeed.folder.isServer) {
+ // If passed the root folder, the caller wants to get the feed's folder
+ // from the db (for cases of an ancestor folder rename/move).
+ let itemResource = FeedUtils.rdf.GetResource(aFeed.url);
+ let ds = FeedUtils.getSubscriptionsDS(aFeed.folder.server);
+ folder = ds.GetTarget(itemResource, FeedUtils.FZ_DESTFOLDER, true);
+ }
+
+ if (this.selectFolder(folder, { parentIndex: aParentIndex }))
+ {
+ let seln = this.mView.selection;
+ let item = this.mView.currentItem;
+ if (item) {
+ for (let i = seln.currentIndex + 1; i < this.mView.rowCount; i++) {
+ if (this.mView.getItemAtIndex(i).url == aFeed.url) {
+ this.mView.selection.select(i);
+ this.mView.treeBox.ensureRowIsVisible(i);
+ found = true;
+ break;
+ }
+ }
+ }
+ }
+
+ return found;
+ },
+
+ updateFeedData: function (aItem)
+ {
+ if (!aItem)
+ return;
+
+ let nameValue = document.getElementById("nameValue");
+ let locationValue = document.getElementById("locationValue");
+ let locationValidate = document.getElementById("locationValidate");
+ let selectFolder = document.getElementById("selectFolder");
+ let selectFolderValue = document.getElementById("selectFolderValue");
+ let isServer = aItem.folder && aItem.folder.isServer;
+ let isFolder = aItem.folder && !aItem.folder.isServer;
+ let isFeed = !aItem.container;
+ let server, displayFolder;
+
+ if (isFeed)
+ {
+ // A feed item. Set the feed location and title info.
+ nameValue.value = aItem.name;
+ locationValue.value = aItem.url;
+ locationValidate.removeAttribute("collapsed");
+
+ // Root the location picker to the news & blogs server.
+ server = aItem.parentFolder.server;
+ displayFolder = aItem.parentFolder;
+ }
+ else
+ {
+ // A folder/container item.
+ nameValue.value = "";
+ nameValue.disabled = true;
+ locationValue.value = "";
+ locationValidate.setAttribute("collapsed", true);
+
+ server = aItem.folder.server;
+ displayFolder = aItem.folder;
+ }
+
+ // Common to both folder and feed items.
+ nameValue.disabled = aItem.container;
+ this.setFolderPicker(displayFolder, isFeed);
+
+ // Set quick mode value.
+ document.getElementById("quickMode").checked = aItem.quickMode;
+
+ // Autotag items.
+ let autotagEnable = document.getElementById("autotagEnable");
+ let autotagUsePrefix = document.getElementById("autotagUsePrefix");
+ let autotagPrefix = document.getElementById("autotagPrefix");
+ let categoryPrefsAcct = FeedUtils.getOptionsAcct(server).category;
+ if (isServer)
+ aItem.options = FeedUtils.getOptionsAcct(server);
+ let categoryPrefs = aItem.options ? aItem.options.category : null;
+
+ autotagEnable.checked = categoryPrefs && categoryPrefs.enabled;
+ autotagUsePrefix.checked = categoryPrefs && categoryPrefs.prefixEnabled;
+ autotagUsePrefix.disabled = !autotagEnable.checked;
+ autotagPrefix.disabled = autotagUsePrefix.disabled || !autotagUsePrefix.checked;
+ autotagPrefix.value = categoryPrefs && categoryPrefs.prefix ?
+ categoryPrefs.prefix : "";
+ },
+
+ setFolderPicker: function(aFolder, aIsFeed)
+ {
+ let editFeed = document.getElementById("editFeed");
+ let folderPrettyPath = FeedUtils.getFolderPrettyPath(aFolder);
+ if (!folderPrettyPath)
+ return editFeed.disabled = true;
+
+ let selectFolder = document.getElementById("selectFolder");
+ let selectFolderPopup = document.getElementById("selectFolderPopup");
+ let selectFolderValue = document.getElementById("selectFolderValue");
+
+ selectFolder.setAttribute("hidden", !aIsFeed);
+ selectFolder._folder = aFolder;
+ selectFolderValue.setAttribute("hidden", aIsFeed);
+ selectFolderValue.setAttribute("showfilepath", false);
+
+ if (aIsFeed)
+ {
+ selectFolderPopup._ensureInitialized();
+ selectFolderPopup.selectFolder(aFolder);
+ selectFolder.setAttribute("label", folderPrettyPath);
+ selectFolder.setAttribute("uri", aFolder.URI);
+ }
+ else
+ {
+ selectFolderValue.value = folderPrettyPath;
+ selectFolderValue.setAttribute("prettypath", folderPrettyPath);
+ selectFolderValue.setAttribute("filepath", aFolder.filePath.path);
+ }
+
+ return editFeed.disabled = false;
+ },
+
+ onClickSelectFolderValue: function(aEvent)
+ {
+ let target = aEvent.target;
+ if ((("button" in aEvent) &&
+ (aEvent.button != 0 ||
+ aEvent.originalTarget.localName != "div" ||
+ target.selectionStart != target.selectionEnd)) ||
+ (aEvent.keyCode && aEvent.keyCode != aEvent.DOM_VK_RETURN))
+ return;
+
+ // Toggle between showing prettyPath and absolute filePath.
+ if (target.getAttribute("showfilepath") == "true")
+ {
+ target.setAttribute("showfilepath", false);
+ target.value = target.getAttribute("prettypath");
+ }
+ else
+ {
+ target.setAttribute("showfilepath", true);
+ target.value = target.getAttribute("filepath");
+ }
+ },
+
+ setNewFolder: function(aEvent)
+ {
+ aEvent.stopPropagation();
+ this.setFolderPicker(aEvent.target._folder, true);
+ this.editFeed();
+ },
+
+ setSummary: function(aChecked)
+ {
+ let item = this.mView.currentItem;
+ if (!item || !item.folder)
+ // Not a folder.
+ return;
+
+ if (item.folder.isServer)
+ {
+ if (document.getElementById("locationValue").value)
+ // Intent is to add a feed/folder to the account, so return.
+ return;
+
+ // An account folder. If it changes, all non feed containing subfolders
+ // need to be updated with the new default.
+ item.folder.server.setBoolValue("quickMode", aChecked);
+ this.FolderListener.folderAdded(item.folder);
+ }
+ else if (!FeedUtils.getFeedUrlsInFolder(item.folder))
+ // Not a folder with feeds.
+ return;
+ else
+ {
+ let feedsInFolder = this.getFeedsInFolder(item.folder);
+ // Update the feeds database, for each feed in the folder.
+ feedsInFolder.forEach(function(feed) { feed.quickMode = aChecked; });
+ // Update the folder's feeds properties in the tree map.
+ item.children.forEach(function(feed) { feed.quickMode = aChecked; });
+ let ds = FeedUtils.getSubscriptionsDS(item.folder.server);
+ ds.Flush();
+ }
+
+ // Update the folder in the tree map.
+ item.quickMode = aChecked;
+ let message = FeedUtils.strings.GetStringFromName("subscribe-feedUpdated");
+ this.updateStatusItem("statusText", message);
+ },
+
+ setCategoryPrefs: function(aNode)
+ {
+ let item = this.mView.currentItem;
+ if (!item)
+ return;
+
+ let isServer = item.folder && item.folder.isServer;
+ let isFolder = item.folder && !item.folder.isServer;
+ let autotagEnable = document.getElementById("autotagEnable");
+ let autotagUsePrefix = document.getElementById("autotagUsePrefix");
+ let autotagPrefix = document.getElementById("autotagPrefix");
+ if (isFolder || (isServer && document.getElementById("locationValue").value))
+ {
+ // Intend to subscribe a feed to a folder, a value must be in the url
+ // field. Update states for addFeed() and return.
+ autotagUsePrefix.disabled = !autotagEnable.checked;
+ autotagPrefix.disabled = autotagUsePrefix.disabled || !autotagUsePrefix.checked;
+ return;
+ }
+
+ switch (aNode.id) {
+ case "autotagEnable":
+ item.options.category.enabled = aNode.checked;
+ break;
+ case "autotagUsePrefix":
+ item.options.category.prefixEnabled = aNode.checked;
+ item.options.category.prefix = autotagPrefix.value;
+ break;
+ }
+
+ if (isServer)
+ {
+ FeedUtils.setOptionsAcct(item.folder.server, item.options)
+ }
+ else
+ {
+ let feedResource = FeedUtils.rdf.GetResource(item.url);
+ let feed = new Feed(feedResource, item.parentFolder.server);
+ feed.options = item.options;
+ let ds = FeedUtils.getSubscriptionsDS(item.parentFolder.server);
+ ds.Flush();
+ }
+
+ this.updateFeedData(item);
+ let message = FeedUtils.strings.GetStringFromName("subscribe-feedUpdated");
+ this.updateStatusItem("statusText", message);
+ },
+
+ onKeyPress: function(aEvent)
+ {
+ if (aEvent.keyCode == aEvent.DOM_VK_DELETE &&
+ aEvent.target.id == "rssSubscriptionsList")
+ this.removeFeed(true);
+
+ this.clearStatusInfo();
+ },
+
+ onSelect: function ()
+ {
+ let item = this.mView.currentItem;
+ this.updateFeedData(item);
+ this.setSummaryFocus();
+ this.updateButtons(item);
+ },
+
+ updateButtons: function (aSelectedItem)
+ {
+ let item = aSelectedItem;
+ let isServer = item && item.folder && item.folder.isServer;
+ let disable = !item || !item.container || isServer ||
+ this.mActionMode == this.kImportingOPML;
+ document.getElementById("addFeed").disabled = disable;
+ disable = !item || (item.container && !isServer) ||
+ this.mActionMode == this.kImportingOPML;
+ document.getElementById("editFeed").disabled = disable;
+ disable = !item || item.container ||
+ this.mActionMode == this.kImportingOPML;
+ document.getElementById("removeFeed").disabled = disable;
+ disable = !item || !isServer ||
+ this.mActionMode == this.kImportingOPML;
+ document.getElementById("importOPML").disabled = disable;
+ document.getElementById("exportOPML").disabled = disable;
+ },
+
+ onMouseDown: function (aEvent)
+ {
+ if (aEvent.button != 0 ||
+ aEvent.target.id == "validationText" ||
+ aEvent.target.id == "addCertException")
+ return;
+
+ this.clearStatusInfo();
+ },
+
+ setSummaryFocus: function ()
+ {
+ let item = this.mView.currentItem;
+ if (!item)
+ return;
+
+ let locationValue = document.getElementById("locationValue");
+ let quickMode = document.getElementById("quickMode");
+ let autotagEnable = document.getElementById("autotagEnable");
+ let autotagUsePrefix = document.getElementById("autotagUsePrefix");
+ let autotagPrefix = document.getElementById("autotagPrefix");
+ let isServer = item.folder && item.folder.isServer;
+ let isFolder = item.folder && !item.folder.isServer;
+ let isFeed = !item.container;
+
+ // Enable summary/autotag by default.
+ quickMode.disabled = autotagEnable.disabled = false;
+ autotagUsePrefix.disabled = !autotagEnable.checked;
+ autotagPrefix.disabled = autotagUsePrefix.disabled || !autotagUsePrefix.checked;
+
+ if (isServer)
+ {
+ let disable = locationValue.hasAttribute("focused") || locationValue.value;
+ document.getElementById("addFeed").disabled = !disable;
+ document.getElementById("editFeed").disabled = disable;
+
+ }
+ else if (isFolder)
+ {
+ if (!locationValue.hasAttribute("focused") && !locationValue.value)
+ {
+ // Enabled for a folder with feeds. Autotag disabled unless intent is
+ // to add a feed.
+ quickMode.disabled = !FeedUtils.getFeedUrlsInFolder(item.folder);
+ autotagEnable.disabled = autotagUsePrefix.disabled =
+ autotagPrefix.disabled = true;
+ }
+ }
+ else
+ {
+ // Summary is per folder.
+ quickMode.disabled = true;
+ }
+ },
+
+ removeFeed: function (aPrompt)
+ {
+ let seln = this.mView.selection;
+ if (seln.count != 1)
+ return;
+
+ let itemToRemove = this.mView.getItemAtIndex(seln.currentIndex);
+
+ if (!itemToRemove || itemToRemove.container)
+ return;
+
+ if (aPrompt)
+ {
+ // Confirm unsubscribe prompt.
+ let pTitle = FeedUtils.strings.GetStringFromName(
+ "subscribe-confirmFeedDeletionTitle");
+ let pMessage = FeedUtils.strings.formatStringFromName(
+ "subscribe-confirmFeedDeletion", [itemToRemove.name], 1);
+ if (Services.prompt.confirmEx(window, pTitle, pMessage,
+ Ci.nsIPromptService.STD_YES_NO_BUTTONS,
+ null, null, null, null, { }))
+ return;
+ }
+
+ FeedUtils.deleteFeed(FeedUtils.rdf.GetResource(itemToRemove.url),
+ itemToRemove.parentFolder.server,
+ itemToRemove.parentFolder);
+
+ // Now that we have removed the feed from the datasource, it is time to
+ // update our view layer. Update parent folder's quickMode if necessary
+ // and remove the child from its parent folder object.
+ let parentIndex = this.mView.getParentIndex(seln.currentIndex);
+ let parentItem = this.mView.getItemAtIndex(parentIndex);
+ this.updateFolderQuickModeInView(itemToRemove, parentItem, true);
+ this.mView.removeItemAtIndex(seln.currentIndex, false);
+ let message = FeedUtils.strings.GetStringFromName("subscribe-feedRemoved");
+ this.updateStatusItem("statusText", message);
+ },
+
+
+ /**
+ * This addFeed is used by 1) Add button, 1) Update button, 3) Drop of a
+ * feed url on a folder (which can be an add or move). If Update, the new
+ * url is added and the old removed; thus aParse is false and no new messages
+ * are downloaded, the feed is only validated and stored in the db. If dnd,
+ * the drop folder is selected and the url is prefilled, so proceed just as
+ * though the url were entered manually. This allows a user to see the dnd
+ * url better in case of errors.
+ *
+ * @param [aFeedLocation] string - the feed url; get the url from the
+ * input field if null.
+ * @param [aFolder] nsIMsgFolder - folder to subscribe, current selected
+ * folder if null.
+ * @param [aParse] boolean - if true (default) parse and download
+ * the feed's articles.
+ * @param [aParams] object - additional params.
+ * @param [aMode] integer - action mode (default is kSubscribeMode)
+ * of the add.
+ *
+ * @return success boolean - true if edit checks passed and an
+ * async download has been initiated.
+ */
+ addFeed: function(aFeedLocation, aFolder, aParse, aParams, aMode)
+ {
+ let message;
+ let parse = aParse == null ? true : aParse;
+ let mode = aMode == null ? this.kSubscribeMode : aMode;
+ let locationValue = document.getElementById("locationValue");
+ let quickMode = aParams && ("quickMode" in aParams) ?
+ aParams.quickMode : document.getElementById("quickMode").checked;
+ let name = aParams && ("name" in aParams) ?
+ aParams.name : document.getElementById("nameValue").value;
+ let options = aParams && ("options" in aParams) ?
+ aParams.options : null;
+
+ if (aFeedLocation)
+ locationValue.value = aFeedLocation;
+ let feedLocation = locationValue.value.trim();
+
+ if (!feedLocation)
+ {
+ message = locationValue.getAttribute("placeholder");
+ this.updateStatusItem("statusText", message);
+ return false;
+ }
+
+ if (!FeedUtils.isValidScheme(feedLocation))
+ {
+ message = FeedUtils.strings.GetStringFromName("subscribe-feedNotValid");
+ this.updateStatusItem("statusText", message);
+ return false;
+ }
+
+ let addFolder;
+ if (aFolder)
+ {
+ // For Update or if passed a folder.
+ if (aFolder instanceof Ci.nsIMsgFolder)
+ addFolder = aFolder;
+ }
+ else
+ {
+ // A folder must be selected for Add and Drop.
+ let index = this.mView.selection.currentIndex;
+ let item = this.mView.getItemAtIndex(index);
+ if (item && item.container)
+ addFolder = item.folder;
+ }
+
+ // Shouldn't happen. Or else not passed an nsIMsgFolder.
+ if (!addFolder)
+ return false;
+
+ // Before we go any further, make sure the user is not already subscribed
+ // to this feed.
+ if (FeedUtils.feedAlreadyExists(feedLocation, addFolder.server))
+ {
+ message = FeedUtils.strings.GetStringFromName(
+ "subscribe-feedAlreadySubscribed");
+ this.updateStatusItem("statusText", message);
+ return false;
+ }
+
+ if (!options)
+ {
+ // Not passed a param, get values from the ui.
+ options = FeedUtils.optionsTemplate;
+ options.category.enabled = document.getElementById("autotagEnable").checked;
+ options.category.prefixEnabled = document.getElementById("autotagUsePrefix").checked;
+ options.category.prefix = document.getElementById("autotagPrefix").value;
+ }
+
+ let folderURI = addFolder.isServer ? null : addFolder.URI;
+ let feedProperties = { feedName : name,
+ feedLocation : feedLocation,
+ folderURI : folderURI,
+ server : addFolder.server,
+ quickMode : quickMode,
+ options : options };
+
+ let feed = this.storeFeed(feedProperties);
+ if (!feed)
+ return false;
+
+ // Now validate and start downloading the feed.
+ message = FeedUtils.strings.GetStringFromName("subscribe-validating-feed");
+ this.updateStatusItem("statusText", message);
+ this.updateStatusItem("progressMeter", 0);
+ document.getElementById("addFeed").setAttribute("disabled", true);
+ this.mActionMode = mode;
+ feed.download(parse, this.mFeedDownloadCallback);
+ return true;
+ },
+
+ // Helper routine used by addFeed and importOPMLFile.
+ storeFeed: function(feedProperties)
+ {
+ let itemResource = FeedUtils.rdf.GetResource(feedProperties.feedLocation);
+ let feed = new Feed(itemResource, feedProperties.server);
+
+ // If the user specified a folder to add the feed to, then set it here.
+ if (feedProperties.folderURI)
+ {
+ let folderResource = FeedUtils.rdf.GetResource(feedProperties.folderURI);
+ if (folderResource)
+ {
+ let folder = folderResource.QueryInterface(Ci.nsIMsgFolder);
+ if (folder && !folder.isServer)
+ feed.folder = folder;
+ }
+ }
+
+ feed.title = feedProperties.feedName;
+ feed.quickMode = feedProperties.quickMode;
+ feed.options = feedProperties.options;
+ return feed;
+ },
+
+ updateAccount: function(aItem)
+ {
+ // Check to see if the categoryPrefs custom prefix string value changed.
+ let editAutotagPrefix = document.getElementById("autotagPrefix").value;
+ if (aItem.options.category.prefix != editAutotagPrefix)
+ {
+ aItem.options.category.prefix = editAutotagPrefix;
+ FeedUtils.setOptionsAcct(aItem.folder.server, aItem.options)
+ let message = FeedUtils.strings.GetStringFromName("subscribe-feedUpdated");
+ this.updateStatusItem("statusText", message);
+ }
+ },
+
+ editFeed: function()
+ {
+ let seln = this.mView.selection;
+ if (seln.count != 1)
+ return;
+
+ let itemToEdit = this.mView.getItemAtIndex(seln.currentIndex);
+ if (itemToEdit.folder && itemToEdit.folder.isServer)
+ {
+ this.updateAccount(itemToEdit)
+ return;
+ }
+
+ if (!itemToEdit || itemToEdit.container || !itemToEdit.parentFolder)
+ return;
+
+ let resource = FeedUtils.rdf.GetResource(itemToEdit.url);
+ let currentFolderServer = itemToEdit.parentFolder.server;
+ let ds = FeedUtils.getSubscriptionsDS(currentFolderServer);
+ let currentFolderURI = itemToEdit.parentFolder.URI;
+ let feed = new Feed(resource, currentFolderServer);
+ feed.folder = itemToEdit.parentFolder;
+
+ let editNameValue = document.getElementById("nameValue").value;
+ let editFeedLocation = document.getElementById("locationValue").value.trim();
+ let selectFolder = document.getElementById("selectFolder");
+ let editQuickMode = document.getElementById("quickMode").checked;
+ let editAutotagPrefix = document.getElementById("autotagPrefix").value;
+
+ if (feed.url != editFeedLocation)
+ {
+ // Updating a url. We need to add the new url and delete the old, to
+ // ensure everything is cleaned up correctly.
+ this.addFeed(null, itemToEdit.parentFolder, false, null, this.kUpdateMode)
+ return;
+ }
+
+ // Did the user change the folder URI for storing the feed?
+ let editFolderURI = selectFolder.getAttribute("uri");
+ if (currentFolderURI != editFolderURI)
+ {
+ // Make sure the new folderpicked folder is visible.
+ this.selectFolder(selectFolder._folder);
+ // Now go back to the feed item.
+ this.selectFeed(feed, null);
+ // We need to find the index of the new parent folder.
+ let newParentIndex = this.mView.kRowIndexUndefined;
+ for (let index = 0; index < this.mView.rowCount; index++)
+ {
+ let item = this.mView.getItemAtIndex(index);
+ if (item && item.container && item.url == editFolderURI)
+ {
+ newParentIndex = index;
+ break;
+ }
+ }
+
+ if (newParentIndex != this.mView.kRowIndexUndefined)
+ this.moveCopyFeed(seln.currentIndex, newParentIndex, "move");
+
+ return;
+ }
+
+ let updated = false;
+ let message = "";
+ // Disable the button until the update completes and we process the async
+ // verify response.
+ document.getElementById("editFeed").setAttribute("disabled", true);
+
+ // Check to see if the title value changed, no blank title allowed.
+ if (feed.title != editNameValue)
+ {
+ if (!editNameValue)
+ {
+ document.getElementById("nameValue").value = feed.title;
+ }
+ else
+ {
+ feed.title = editNameValue;
+ itemToEdit.name = editNameValue;
+ seln.tree.invalidateRow(seln.currentIndex);
+ updated = true;
+ }
+ }
+
+ // Check to see if the quickMode value changed.
+ if (feed.quickMode != editQuickMode)
+ {
+ feed.quickMode = editQuickMode;
+ itemToEdit.quickMode = editQuickMode;
+ updated = true;
+ }
+
+ // Check to see if the categoryPrefs custom prefix string value changed.
+ if (itemToEdit.options.category.prefix != editAutotagPrefix &&
+ itemToEdit.options.category.prefix != null &&
+ editAutotagPrefix != "")
+ {
+ itemToEdit.options.category.prefix = editAutotagPrefix;
+ feed.options = itemToEdit.options;
+ updated = true;
+ }
+
+ let verifyDelay = 0;
+ if (updated) {
+ ds.Flush();
+ message = FeedUtils.strings.GetStringFromName("subscribe-feedUpdated");
+ this.updateStatusItem("statusText", message);
+ verifyDelay = 1500;
+ }
+
+ // Now we want to verify if the stored feed url still works. If it
+ // doesn't, show the error. Delay a bit to leave Updated message visible.
+ message = FeedUtils.strings.GetStringFromName("subscribe-validating-feed");
+ this.mActionMode = this.kVerifyUrlMode;
+ setTimeout(() => {
+ this.updateStatusItem("statusText", message);
+ this.updateStatusItem("progressMeter", "?");
+ feed.download(false, this.mFeedDownloadCallback);
+ }, verifyDelay);
+ },
+
+/**
+ * Moves or copies a feed to another folder or account.
+ *
+ * @param int aOldFeedIndex - index in tree of target feed item.
+ * @param int aNewParentIndex - index in tree of target parent folder item.
+ * @param string aMoveCopy - either "move" or "copy".
+ */
+ moveCopyFeed: function(aOldFeedIndex, aNewParentIndex, aMoveCopy)
+ {
+ let moveFeed = aMoveCopy == "move";
+ let currentItem = this.mView.getItemAtIndex(aOldFeedIndex);
+ if (!currentItem ||
+ this.mView.getParentIndex(aOldFeedIndex) == aNewParentIndex)
+ // If the new parent is the same as the current parent, then do nothing.
+ return;
+
+ let currentParentIndex = this.mView.getParentIndex(aOldFeedIndex);
+ let currentParentItem = this.mView.getItemAtIndex(currentParentIndex);
+ let currentParentResource = FeedUtils.rdf.GetResource(currentParentItem.url);
+ let currentFolder = currentParentResource.QueryInterface(Ci.nsIMsgFolder);
+
+ let newParentItem = this.mView.getItemAtIndex(aNewParentIndex);
+ let newParentResource = FeedUtils.rdf.GetResource(newParentItem.url);
+ let newFolder = newParentResource.QueryInterface(Ci.nsIMsgFolder);
+
+ let ds = FeedUtils.getSubscriptionsDS(currentItem.parentFolder.server);
+ let resource = FeedUtils.rdf.GetResource(currentItem.url);
+
+ let accountMoveCopy = false;
+ if (currentFolder.rootFolder.URI == newFolder.rootFolder.URI)
+ {
+ // Moving within the same account/feeds db.
+ if (newFolder.isServer || !moveFeed)
+ // No moving to account folder if already in the account; can only move,
+ // not copy, to folder in the same account.
+ return;
+
+ // Unassert the older URI, add an assertion for the new parent URI.
+ ds.Change(resource, FeedUtils.FZ_DESTFOLDER,
+ currentParentResource, newParentResource);
+ ds.Flush();
+
+ // Update folderpane favicons.
+ FeedUtils.setFolderPaneProperty(currentFolder, "favicon", null, "row");
+ FeedUtils.setFolderPaneProperty(newFolder, "favicon", null, "row");
+ }
+ else
+ {
+ // Moving/copying to a new account. If dropping on the account folder,
+ // a new subfolder is created if necessary.
+ accountMoveCopy = true;
+ let mode = moveFeed ? this.kMoveMode : this.kCopyMode;
+ let params = {quickMode: currentItem.quickMode,
+ name: currentItem.name,
+ options: currentItem.options};
+ // Subscribe to the new folder first. If it already exists in the
+ // account or on error, return.
+ if (!this.addFeed(currentItem.url, newFolder, false, params, mode))
+ return;
+ // Unsubscribe the feed from the old folder, if add to the new folder
+ // is successfull, and doing a move.
+ if (moveFeed)
+ FeedUtils.deleteFeed(FeedUtils.rdf.GetResource(currentItem.url),
+ currentItem.parentFolder.server,
+ currentItem.parentFolder);
+ }
+
+ // Update local favicons.
+ currentParentItem.favicon = newParentItem.favicon = null;
+
+ // Finally, update our view layer. Update old parent folder's quickMode
+ // and remove the old row, if move. Otherwise no change to the view.
+ if (moveFeed)
+ {
+ this.updateFolderQuickModeInView(currentItem, currentParentItem, true);
+ this.mView.removeItemAtIndex(aOldFeedIndex, true);
+ if (aNewParentIndex > aOldFeedIndex)
+ aNewParentIndex--;
+ }
+
+ if (accountMoveCopy)
+ {
+ // If a cross account move/copy, download callback will update the view
+ // with the new location. Preselect folder/mode for callback.
+ this.selectFolder(newFolder, { parentIndex: aNewParentIndex });
+ return;
+ }
+
+ // Add the new row location to the view.
+ currentItem.level = newParentItem.level + 1;
+ currentItem.parentFolder = newFolder;
+ this.updateFolderQuickModeInView(currentItem, newParentItem, false);
+ newParentItem.children.push(currentItem);
+
+ if (newParentItem.open)
+ // Close the container, selecting the feed will rebuild the view rows.
+ this.mView.toggle(aNewParentIndex);
+
+ this.selectFeed({folder: newParentItem.folder, url: currentItem.url},
+ aNewParentIndex);
+
+ let message = FeedUtils.strings.GetStringFromName("subscribe-feedMoved");
+ this.updateStatusItem("statusText", message);
+ },
+
+ updateFolderQuickModeInView: function (aFeedItem, aParentItem, aRemove)
+ {
+ let feedItem = aFeedItem;
+ let parentItem = aParentItem;
+ let feedUrlArray = FeedUtils.getFeedUrlsInFolder(feedItem.parentFolder);
+ let feedsInFolder = feedUrlArray ? feedUrlArray.length : 0;
+
+ if (aRemove && feedsInFolder < 1)
+ // Removed only feed in folder; set quickMode to server default.
+ parentItem.quickMode = parentItem.folder.server.getBoolValue("quickMode");
+
+ if (!aRemove)
+ {
+ // Just added a feed to a folder. If there are already feeds in the
+ // folder, the feed must reflect the parent's quickMode. If it is the
+ // only feed, update the parent folder to the feed's quickMode.
+ if (feedsInFolder > 1)
+ {
+ let feedResource = FeedUtils.rdf.GetResource(feedItem.url);
+ let feed = new Feed(feedResource, feedItem.parentFolder.server);
+ feed.quickMode = parentItem.quickMode;
+ feedItem.quickMode = parentItem.quickMode;
+ }
+ else
+ parentItem.quickMode = feedItem.quickMode;
+ }
+ },
+
+ onDragStart: function (aEvent)
+ {
+ // Get the selected feed article (if there is one).
+ let seln = this.mView.selection;
+ if (seln.count != 1)
+ return;
+
+ // Only initiate a drag if the item is a feed (ignore folders/containers).
+ let item = this.mView.getItemAtIndex(seln.currentIndex);
+ if (!item || item.container)
+ return;
+
+ aEvent.dataTransfer.setData("text/x-moz-feed-index", seln.currentIndex);
+ aEvent.dataTransfer.effectAllowed = "copyMove";
+ },
+
+ onDragOver: function (aEvent)
+ {
+ this.mView._currentDataTransfer = aEvent.dataTransfer;
+ },
+
+ mFeedDownloadCallback:
+ {
+ mSubscribeMode: true,
+ downloaded: function(feed, aErrorCode)
+ {
+ // Offline check is done in the context of 3pane, return to the subscribe
+ // window once the modal prompt is dispatched.
+ window.focus();
+ // Feed is null if our attempt to parse the feed failed.
+ let message = "";
+ let win = FeedSubscriptions;
+ if (aErrorCode == FeedUtils.kNewsBlogSuccess ||
+ aErrorCode == FeedUtils.kNewsBlogNoNewItems)
+ {
+ win.updateStatusItem("progressMeter", 100);
+
+ if (win.mActionMode == win.kVerifyUrlMode) {
+ // Just checking for errors, if none bye. The (non error) code
+ // kNewsBlogNoNewItems can only happen in verify mode.
+ win.mActionMode = null;
+ win.clearStatusInfo();
+ message = FeedUtils.strings.GetStringFromName("subscribe-feedVerified");
+ win.updateStatusItem("statusText", message);
+ document.getElementById("editFeed").removeAttribute("disabled");
+ return;
+ }
+
+ // Add the feed to the databases.
+ FeedUtils.addFeed(feed);
+
+ // Now add the feed to our view. If adding, the current selection will
+ // be a folder; if updating it will be a feed. No need to rebuild the
+ // entire view, that is too jarring.
+ let curIndex = win.mView.selection.currentIndex;
+ let curItem = win.mView.getItemAtIndex(curIndex);
+ if (curItem)
+ {
+ let parentIndex, parentItem, newItem, level;
+ let rows = win.mFeedContainers;
+ if (curItem.container)
+ {
+ // Open the container, if it exists.
+ let folderExists = win.selectFolder(feed.folder,
+ { parentIndex: curIndex });
+ if (!folderExists)
+ {
+ // This means a new folder was created.
+ parentIndex = curIndex;
+ parentItem = curItem;
+ level = curItem.level + 1;
+ newItem = win.makeFolderObject(feed.folder, level);
+ }
+ else
+ {
+ // If a folder happens to exist which matches one that would
+ // have been created, the feed system reuses it. Get the
+ // current item again if reusing a previously unselected folder.
+ curIndex = win.mView.selection.currentIndex;
+ curItem = win.mView.getItemAtIndex(curIndex);
+ parentIndex = curIndex;
+ parentItem = curItem;
+ level = curItem.level + 1;
+ newItem = win.makeFeedObject(feed, feed.folder, level);
+ }
+ }
+ else
+ {
+ // Adding a feed.
+ parentIndex = win.mView.getParentIndex(curIndex);
+ parentItem = win.mView.getItemAtIndex(parentIndex);
+ level = curItem.level;
+ newItem = win.makeFeedObject(feed, feed.folder, level);
+ }
+
+ if (!newItem.container)
+ win.updateFolderQuickModeInView(newItem, parentItem, false);
+ parentItem.children.push(newItem);
+ parentItem.children = win.folderItemSorter(parentItem.children);
+ parentItem.favicon = null;
+
+ if (win.mActionMode == win.kSubscribeMode)
+ message = FeedUtils.strings.GetStringFromName(
+ "subscribe-feedAdded");
+ if (win.mActionMode == win.kUpdateMode)
+ {
+ win.removeFeed(false);
+ message = FeedUtils.strings.GetStringFromName(
+ "subscribe-feedUpdated");
+ }
+ if (win.mActionMode == win.kMoveMode)
+ message = FeedUtils.strings.GetStringFromName(
+ "subscribe-feedMoved");
+ if (win.mActionMode == win.kCopyMode)
+ message = FeedUtils.strings.GetStringFromName(
+ "subscribe-feedCopied");
+
+ win.selectFeed(feed, parentIndex);
+ }
+ }
+ else
+ {
+ // Non success. Remove intermediate traces from the feeds database.
+ // But only if we're not in verify mode.
+ if (win.mActionMode != win.kVerifyUrlMode &&
+ feed && feed.url && feed.server)
+ FeedUtils.deleteFeed(FeedUtils.rdf.GetResource(feed.url),
+ feed.server,
+ feed.server.rootFolder);
+
+ if (aErrorCode == FeedUtils.kNewsBlogInvalidFeed)
+ message = FeedUtils.strings.GetStringFromName(
+ "subscribe-feedNotValid");
+ if (aErrorCode == FeedUtils.kNewsBlogRequestFailure)
+ message = FeedUtils.strings.GetStringFromName(
+ "subscribe-networkError");
+ if (aErrorCode == FeedUtils.kNewsBlogFileError)
+ message = FeedUtils.strings.GetStringFromName(
+ "subscribe-errorOpeningFile");
+ if (aErrorCode == FeedUtils.kNewsBlogBadCertError) {
+ let host = Services.io.newURI(feed.url, null, null).host;
+ message = FeedUtils.strings.formatStringFromName(
+ "newsblog-badCertError", [host], 1);
+ }
+ if (aErrorCode == FeedUtils.kNewsBlogNoAuthError)
+ message = FeedUtils.strings.GetStringFromName(
+ "subscribe-noAuthError");
+
+ if (win.mActionMode != win.kUpdateMode &&
+ win.mActionMode != win.kVerifyUrlMode)
+ // Re-enable the add button if subscribe failed.
+ document.getElementById("addFeed").removeAttribute("disabled");
+ if (win.mActionMode == win.kVerifyUrlMode)
+ // Re-enable the update button if verify failed.
+ document.getElementById("editFeed").removeAttribute("disabled");
+ }
+
+ win.mActionMode = null;
+ win.clearStatusInfo();
+ let code = feed.url.startsWith("http") ? aErrorCode : null;
+ win.updateStatusItem("statusText", message, code);
+ },
+
+ // 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)
+ {
+ window.focus();
+ let message = FeedUtils.strings.formatStringFromName(
+ "subscribe-gettingFeedItems",
+ [aCurrentFeedItems, aMaxFeedItems], 2);
+ FeedSubscriptions.updateStatusItem("statusText", message);
+ this.onProgress(feed, aCurrentFeedItems, aMaxFeedItems);
+ },
+
+ onProgress: function(feed, aProgress, aProgressMax, aLengthComputable)
+ {
+ FeedSubscriptions.updateStatusItem("progressMeter",
+ (aProgress * 100) / aProgressMax);
+ }
+ },
+
+ // Status routines.
+ updateStatusItem: function(aID, aValue, aErrorCode)
+ {
+ let el = document.getElementById(aID);
+ if (el.getAttribute("collapsed"))
+ el.removeAttribute("collapsed");
+
+ if (aID == "progressMeter")
+ el.setAttribute("mode", aValue == "?" ? "undetermined" : "determined");
+
+ if (aID == "statusText")
+ el.textContent = aValue;
+ else
+ el.value = aValue;
+
+ el = document.getElementById("validationText");
+ if (aErrorCode == FeedUtils.kNewsBlogInvalidFeed)
+ el.removeAttribute("collapsed");
+ else
+ el.setAttribute("collapsed", true);
+
+ el = document.getElementById("addCertException");
+ if (aErrorCode == FeedUtils.kNewsBlogBadCertError)
+ el.removeAttribute("collapsed");
+ else
+ el.setAttribute("collapsed", true);
+ },
+
+ clearStatusInfo: function()
+ {
+ document.getElementById("statusText").textContent = "";
+ document.getElementById("progressMeter").collapsed = true;
+ document.getElementById("validationText").collapsed = true;
+ document.getElementById("addCertException").collapsed = true;
+ },
+
+ checkValidation: function(aEvent)
+ {
+ if (aEvent.button != 0)
+ return;
+
+ let validationSite = "http://validator.w3.org";
+ let validationQuery = "http://validator.w3.org/feed/check.cgi?url=";
+
+ if (this.mMainWin)
+ {
+ let tabmail = this.mMainWin.document.getElementById("tabmail");
+ if (tabmail)
+ {
+ let feedLocation = document.getElementById("locationValue").value;
+ let url = validationQuery + encodeURIComponent(feedLocation);
+
+ this.mMainWin.focus();
+ this.mMainWin.openContentTab(url, "tab", "^" + validationSite);
+ FeedUtils.log.debug("checkValidation: query url - " + url);
+ }
+ }
+ aEvent.stopPropagation();
+ },
+
+ addCertExceptionDialog: function()
+ {
+ let feedURL = document.getElementById("locationValue").value.trim();
+ let params = { exceptionAdded : false,
+ location: feedURL,
+ prefetchCert: true };
+ window.openDialog("chrome://pippki/content/exceptionDialog.xul",
+ "", "chrome,centerscreen,modal", params);
+ if (params.exceptionAdded)
+ this.clearStatusInfo();
+ },
+
+ // Listener for folder pane changes.
+ FolderListener: {
+ get feedWindow() {
+ let subscriptionsWindow =
+ Services.wm.getMostRecentWindow("Mail:News-BlogSubscriptions");
+ return subscriptionsWindow ? subscriptionsWindow.FeedSubscriptions : null;
+ },
+
+ get currentSelectedIndex() {
+ return this.feedWindow ? this.feedWindow.mView.selection.currentIndex : -1;
+ },
+
+ get currentSelectedItem() {
+ return this.feedWindow ? this.feedWindow.mView.currentItem : null;
+ },
+
+ folderAdded: function(aFolder)
+ {
+ if (aFolder.server.type != "rss" ||
+ FeedUtils.isInTrash(aFolder))
+ return;
+
+ let parentFolder = aFolder.isServer ? aFolder : aFolder.parent;
+ FeedUtils.log.debug("folderAdded: folder:parent - " + aFolder.name + ":" +
+ (parentFolder ? parentFolder.filePath.path : "(null)"));
+
+ if (!parentFolder || !this.feedWindow)
+ return;
+
+ let feedWindow = this.feedWindow;
+ let curSelIndex = this.currentSelectedIndex;
+ let curSelItem = this.currentSelectedItem;
+ let firstVisRow = feedWindow.mView.treeBox.getFirstVisibleRow();
+ let indexInView = feedWindow.mView.getItemInViewIndex(parentFolder);
+ let open = indexInView != null;
+
+ if (aFolder.isServer)
+ {
+ if (indexInView != null)
+ // Existing account root folder in the view.
+ open = feedWindow.mView.getItemAtIndex(indexInView).open;
+ else
+ {
+ // Add the account root folder to the view.
+ feedWindow.mFeedContainers.push(feedWindow.makeFolderObject(parentFolder, 0));
+ feedWindow.mView.mRowCount++;
+ feedWindow.mTree.view = feedWindow.mView;
+ feedWindow.mView.treeBox.scrollToRow(firstVisRow);
+ return;
+ }
+ }
+
+ // Rebuild the added folder's parent item in the tree row cache.
+ feedWindow.selectFolder(parentFolder, { select: false,
+ open: open,
+ newFolder: parentFolder });
+
+ if (indexInView == null || !curSelItem)
+ // Folder isn't in the tree view, no need to update the view.
+ return;
+
+ let parentIndex = feedWindow.mView.getParentIndex(indexInView);
+ if (parentIndex == feedWindow.mView.kRowIndexUndefined)
+ // Root folder is its own parent.
+ parentIndex = indexInView;
+ if (open)
+ {
+ // Close an open parent (or root) folder.
+ feedWindow.mView.toggle(parentIndex);
+ feedWindow.mView.toggleOpenState(parentIndex);
+ }
+ feedWindow.mView.treeBox.scrollToRow(firstVisRow);
+
+ if (curSelItem.container)
+ feedWindow.selectFolder(curSelItem.folder, { open: curSelItem.open });
+ else
+ feedWindow.selectFeed({ folder: curSelItem.parentFolder,
+ url: curSelItem.url }, parentIndex);
+ },
+
+ folderDeleted: function(aFolder)
+ {
+ if (aFolder.server.type != "rss" || FeedUtils.isInTrash(aFolder))
+ return;
+
+ FeedUtils.log.debug("folderDeleted: folder - " + aFolder.name);
+ if (!this.feedWindow)
+ return;
+
+ let feedWindow = this.feedWindow;
+ let curSelIndex = this.currentSelectedIndex;
+ let indexInView = feedWindow.mView.getItemInViewIndex(aFolder);
+ let open = indexInView != null;
+
+ // Delete the folder from the tree row cache.
+ feedWindow.selectFolder(aFolder, { select: false, open: false, remove: true });
+
+ if (!open || curSelIndex < 0)
+ // Folder isn't in the tree view, no need to update the view.
+ return;
+
+ let select =
+ indexInView == curSelIndex ||
+ feedWindow.mView.isIndexChildOfParentIndex(indexInView, curSelIndex);
+ feedWindow.mView.removeItemAtIndex(indexInView, !select);
+ },
+
+ folderRenamed: function(aOrigFolder, aNewFolder)
+ {
+ if (aNewFolder.server.type != "rss" || FeedUtils.isInTrash(aNewFolder))
+ return;
+
+ FeedUtils.log.debug("folderRenamed: old:new - " +
+ aOrigFolder.name + ":" + aNewFolder.name);
+ if (!this.feedWindow)
+ return;
+
+ let feedWindow = this.feedWindow;
+ let curSelIndex = this.currentSelectedIndex;
+ let curSelItem = this.currentSelectedItem;
+ let firstVisRow = feedWindow.mView.treeBox.getFirstVisibleRow();
+ let indexInView = feedWindow.mView.getItemInViewIndex(aOrigFolder);
+ let open = indexInView != null;
+
+ // Rebuild the renamed folder's item in the tree row cache.
+ feedWindow.selectFolder(aOrigFolder, { select: false,
+ open: open,
+ newFolder: aNewFolder });
+
+ if (!open || !curSelItem)
+ // Folder isn't in the tree view, no need to update the view.
+ return;
+
+ let select =
+ indexInView == curSelIndex ||
+ feedWindow.mView.isIndexChildOfParentIndex(indexInView, curSelIndex);
+ let parentIndex = feedWindow.mView.getParentIndex(indexInView);
+ if (parentIndex == feedWindow.mView.kRowIndexUndefined)
+ // Root folder is its own parent.
+ parentIndex = indexInView;
+ feedWindow.mView.toggle(parentIndex);
+ feedWindow.mView.toggleOpenState(parentIndex);
+ feedWindow.mView.treeBox.scrollToRow(firstVisRow);
+
+ if (curSelItem.container) {
+ if (curSelItem.folder == aOrigFolder)
+ feedWindow.selectFolder(aNewFolder, { open: curSelItem.open });
+ else if (select)
+ feedWindow.mView.selection.select(indexInView);
+ else
+ feedWindow.selectFolder(curSelItem.folder, { open: curSelItem.open });
+ }
+ else
+ feedWindow.selectFeed({ folder: curSelItem.parentFolder.rootFolder,
+ url: curSelItem.url }, parentIndex);
+ },
+
+ folderMoveCopyCompleted: function(aMove, aSrcFolder, aDestFolder)
+ {
+ if (aDestFolder.server.type != "rss")
+ return;
+
+ FeedUtils.log.debug("folderMoveCopyCompleted: move:src:dest - " +
+ aMove + ":" + aSrcFolder.name + ":" + aDestFolder.name);
+ if (!this.feedWindow)
+ return;
+
+ let feedWindow = this.feedWindow;
+ let curSelIndex = this.currentSelectedIndex;
+ let curSelItem = this.currentSelectedItem;
+ let firstVisRow = feedWindow.mView.treeBox.getFirstVisibleRow();
+ let indexInView = feedWindow.mView.getItemInViewIndex(aSrcFolder);
+ let destIndexInView = feedWindow.mView.getItemInViewIndex(aDestFolder);
+ let open = indexInView != null || destIndexInView != null;
+ let parentIndex = feedWindow.mView.getItemInViewIndex(aDestFolder.parent ||
+ aDestFolder);
+ let select =
+ indexInView == curSelIndex ||
+ feedWindow.mView.isIndexChildOfParentIndex(indexInView, curSelIndex);
+
+ if (aMove)
+ {
+ this.folderDeleted(aSrcFolder);
+ if (aDestFolder.getFlag(Ci.nsMsgFolderFlags.Trash))
+ return;
+ }
+
+ setTimeout(function() {
+ // State on disk needs to settle before a folder object can be rebuilt.
+ feedWindow.selectFolder(aDestFolder, { select: false,
+ open: open || select,
+ newFolder: aDestFolder });
+
+ if (!open || !curSelItem)
+ // Folder isn't in the tree view, no need to update the view.
+ return;
+
+ feedWindow.mView.toggle(parentIndex);
+ feedWindow.mView.toggleOpenState(parentIndex);
+ feedWindow.mView.treeBox.scrollToRow(firstVisRow);
+
+ if (curSelItem.container) {
+ if (curSelItem.folder == aSrcFolder || select)
+ feedWindow.selectFolder(aDestFolder, { open: true });
+ else
+ feedWindow.selectFolder(curSelItem.folder, { open: curSelItem.open });
+ }
+ else
+ feedWindow.selectFeed({ folder: curSelItem.parentFolder.rootFolder,
+ url: curSelItem.url }, null);
+ }, 50);
+ }
+ },
+
+ /* *************************************************************** */
+ /* OPML Functions */
+ /* *************************************************************** */
+
+ get brandShortName() {
+ let brandBundle = document.getElementById("bundle_brand");
+ return brandBundle ? brandBundle.getString("brandShortName") : "";
+ },
+
+/**
+ * Export feeds as opml file Save As filepicker function.
+ *
+ * @param bool aList - if true, exporting as list; if false (default)
+ * exporting feeds in folder structure - used for title.
+ * @return nsILocalFile or null.
+ */
+ opmlPickSaveAsFile: function(aList)
+ {
+ let accountName = this.mRSSServer.rootFolder.prettyName;
+ let fileName = FeedUtils.strings.formatStringFromName(
+ "subscribe-OPMLExportDefaultFileName",
+ [this.brandShortName, accountName], 2);
+ let title = aList ? FeedUtils.strings.formatStringFromName(
+ "subscribe-OPMLExportTitleList", [accountName], 1) :
+ FeedUtils.strings.formatStringFromName(
+ "subscribe-OPMLExportTitleStruct", [accountName], 1);
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+
+ fp.defaultString = fileName;
+ fp.defaultExtension = "opml";
+ if (this.opmlLastSaveAsDir && (this.opmlLastSaveAsDir instanceof Ci.nsILocalFile))
+ fp.displayDirectory = this.opmlLastSaveAsDir;
+
+ let opmlFilterText = FeedUtils.strings.GetStringFromName(
+ "subscribe-OPMLExportOPMLFilesFilterText");
+ fp.appendFilter(opmlFilterText, "*.opml");
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+ fp.filterIndex = 0;
+ fp.init(window, title, Ci.nsIFilePicker.modeSave);
+
+ if (fp.show() != Ci.nsIFilePicker.returnCancel && fp.file)
+ {
+ this.opmlLastSaveAsDir = fp.file.parent;
+ return fp.file;
+ }
+
+ return null;
+ },
+
+/**
+ * Import feeds opml file Open filepicker function.
+ *
+ * @return nsILocalFile or null.
+ */
+ opmlPickOpenFile: function()
+ {
+ let title = FeedUtils.strings.GetStringFromName("subscribe-OPMLImportTitle");
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+
+ fp.defaultString = "";
+ if (this.opmlLastOpenDir && (this.opmlLastOpenDir instanceof Ci.nsILocalFile))
+ fp.displayDirectory = this.opmlLastOpenDir;
+
+ let opmlFilterText = FeedUtils.strings.GetStringFromName(
+ "subscribe-OPMLExportOPMLFilesFilterText");
+ fp.appendFilter(opmlFilterText, "*.opml");
+ fp.appendFilters(Ci.nsIFilePicker.filterXML);
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+ fp.init(window, title, Ci.nsIFilePicker.modeOpen);
+
+ if (fp.show() != Ci.nsIFilePicker.returnCancel && fp.file)
+ {
+ this.opmlLastOpenDir = fp.file.parent;
+ return fp.file;
+ }
+
+ return null;
+ },
+
+ exportOPML: function(aEvent)
+ {
+ // Account folder must be selected.
+ let item = this.mView.currentItem;
+ if (!item || !item.folder || !item.folder.isServer)
+ return;
+
+ this.mRSSServer = item.folder.server;
+ let rootFolder = this.mRSSServer.rootFolder;
+ let exportAsList = aEvent.ctrlKey;
+ let SPACES2 = " ";
+ let SPACES4 = " ";
+
+ if (this.mRSSServer.rootFolder.hasSubFolders)
+ {
+ let opmlDoc = document.implementation.createDocument("", "opml", null);
+ let opmlRoot = opmlDoc.documentElement;
+ opmlRoot.setAttribute("version", "1.0");
+ opmlRoot.setAttribute("xmlns:fz", "urn:forumzilla:");
+
+ this.generatePPSpace(opmlRoot, SPACES2);
+
+ // Make the <head> element.
+ let head = opmlDoc.createElement("head");
+ this.generatePPSpace(head, SPACES4);
+ let titleText = FeedUtils.strings.formatStringFromName(
+ "subscribe-OPMLExportFileDialogTitle",
+ [this.brandShortName, rootFolder.prettyName], 2);
+ let title = opmlDoc.createElement("title");
+ title.appendChild(opmlDoc.createTextNode(titleText));
+ head.appendChild(title);
+ this.generatePPSpace(head, SPACES4);
+ let dt = opmlDoc.createElement("dateCreated");
+ dt.appendChild(opmlDoc.createTextNode((new Date()).toUTCString()));
+ head.appendChild(dt);
+ this.generatePPSpace(head, SPACES2);
+ opmlRoot.appendChild(head);
+
+ this.generatePPSpace(opmlRoot, SPACES2);
+
+ // Add <outline>s to the <body>.
+ let body = opmlDoc.createElement("body");
+ if (exportAsList)
+ this.generateOutlineList(rootFolder, body, SPACES4.length + 2);
+ else
+ this.generateOutlineStruct(rootFolder, body, SPACES4.length);
+
+ this.generatePPSpace(body, SPACES2);
+
+ if (!body.childElementCount)
+ // No folders/feeds.
+ return;
+
+ opmlRoot.appendChild(body);
+ this.generatePPSpace(opmlRoot, "");
+
+ let serializer = new XMLSerializer();
+
+ if (FeedUtils.log.level <= Log4Moz.Level.Debug)
+ FeedUtils.log.debug("exportOPML: opmlDoc -\n" +
+ serializer.serializeToString(opmlDoc) + "\n");
+
+ // Get file to save from filepicker.
+ let saveAsFile = this.opmlPickSaveAsFile(exportAsList);
+ if (!saveAsFile)
+ return;
+
+ let fos = FileUtils.openSafeFileOutputStream(saveAsFile);
+ serializer.serializeToStream(opmlDoc, fos, "utf-8");
+ FileUtils.closeSafeFileOutputStream(fos);
+
+ let statusReport = FeedUtils.strings.formatStringFromName(
+ "subscribe-OPMLExportDone", [saveAsFile.path], 1);
+ this.updateStatusItem("statusText", statusReport);
+ }
+ },
+
+ generatePPSpace: function(aNode, indentString)
+ {
+ aNode.appendChild(aNode.ownerDocument.createTextNode("\n"));
+ aNode.appendChild(aNode.ownerDocument.createTextNode(indentString));
+ },
+
+ generateOutlineList: function(baseFolder, parent, indentLevel)
+ {
+ // Pretty printing.
+ let indentString = " ".repeat(indentLevel - 2);
+
+ let feedOutline;
+ let folderEnumerator = baseFolder.subFolders;
+ while (folderEnumerator.hasMoreElements())
+ {
+ let folder = folderEnumerator.getNext().QueryInterface(Ci.nsIMsgFolder);
+ FeedUtils.log.debug("generateOutlineList: folder - " +
+ folder.filePath.path);
+ if (!(folder instanceof Ci.nsIMsgFolder) ||
+ folder.getFlag(Ci.nsMsgFolderFlags.Trash) ||
+ folder.getFlag(Ci.nsMsgFolderFlags.Virtual))
+ continue;
+
+ FeedUtils.log.debug("generateOutlineList: CONTINUE folderName - " +
+ folder.name);
+
+ if (folder.hasSubFolders)
+ {
+ FeedUtils.log.debug("generateOutlineList: has subfolders - " +
+ folder.name);
+ // Recurse.
+ this.generateOutlineList(folder, parent, indentLevel);
+ }
+
+ // Add outline elements with xmlUrls.
+ let feeds = this.getFeedsInFolder(folder);
+ for (let feed of feeds)
+ {
+ FeedUtils.log.debug("generateOutlineList: folder has FEED url - " +
+ folder.name + " : " + feed.url);
+ feedOutline = this.exportOPMLOutline(feed, parent.ownerDocument);
+ this.generatePPSpace(parent, indentString);
+ parent.appendChild(feedOutline);
+ }
+ }
+ },
+
+ generateOutlineStruct: function(baseFolder, parent, indentLevel)
+ {
+ // Pretty printing.
+ function indentString(len) { return " ".repeat(len - 2); };
+
+ let folderOutline, feedOutline;
+ let folderEnumerator = baseFolder.subFolders;
+ while (folderEnumerator.hasMoreElements())
+ {
+ let folder = folderEnumerator.getNext().QueryInterface(Ci.nsIMsgFolder);
+ FeedUtils.log.debug("generateOutlineStruct: folder - " +
+ folder.filePath.path);
+ if (!(folder instanceof Ci.nsIMsgFolder) ||
+ folder.getFlag(Ci.nsMsgFolderFlags.Trash) ||
+ folder.getFlag(Ci.nsMsgFolderFlags.Virtual))
+ continue;
+
+ FeedUtils.log.debug("generateOutlineStruct: CONTINUE folderName - " +
+ folder.name);
+
+ // Make a folder outline element.
+ folderOutline = parent.ownerDocument.createElement("outline");
+ folderOutline.setAttribute("title", folder.prettyName);
+ this.generatePPSpace(parent, indentString(indentLevel + 2));
+
+ if (folder.hasSubFolders)
+ {
+ FeedUtils.log.debug("generateOutlineStruct: has subfolders - " +
+ folder.name);
+ // Recurse.
+ this.generateOutlineStruct(folder, folderOutline, indentLevel + 2);
+ }
+
+ let feeds = this.getFeedsInFolder(folder);
+ for (let feed of feeds)
+ {
+ // Add feed outline elements with xmlUrls.
+ FeedUtils.log.debug("generateOutlineStruct: folder has FEED url - "+
+ folder.name + " : " + feed.url);
+ feedOutline = this.exportOPMLOutline(feed, parent.ownerDocument);
+ this.generatePPSpace(folderOutline, indentString(indentLevel + 4));
+ folderOutline.appendChild(feedOutline);
+ }
+
+ parent.appendChild(folderOutline);
+ }
+ },
+
+ exportOPMLOutline: function(aFeed, aDoc)
+ {
+ let outRv = aDoc.createElement("outline");
+ outRv.setAttribute("type", "rss");
+ outRv.setAttribute("title", aFeed.title);
+ outRv.setAttribute("text", aFeed.title);
+ outRv.setAttribute("version", "RSS");
+ outRv.setAttribute("fz:quickMode", aFeed.quickMode);
+ outRv.setAttribute("fz:options", JSON.stringify(aFeed.options));
+ outRv.setAttribute("xmlUrl", aFeed.url);
+ outRv.setAttribute("htmlUrl", aFeed.link);
+ return outRv;
+ },
+
+ importOPML: function()
+ {
+ // Account folder must be selected in subscribe dialog.
+ let item = this.mView ? this.mView.currentItem : null;
+ if (!item || !item.folder || !item.folder.isServer)
+ return;
+
+ let server = item.folder.server;
+ // Get file to open from filepicker.
+ let openFile = this.opmlPickOpenFile();
+ if (!openFile)
+ return;
+
+ this.mActionMode = this.kImportingOPML;
+ this.updateButtons(null);
+ this.selectFolder(item.folder, { select: false, open: true });
+ let statusReport = FeedUtils.strings.GetStringFromName("subscribe-loading");
+ this.updateStatusItem("statusText", statusReport);
+ // If there were a getElementsByAttribute in html, we could go determined...
+ this.updateStatusItem("progressMeter", "?");
+
+ if (!this.importOPMLFile(openFile, server, this.importOPMLFinished)) {
+ this.mActionMode = null;
+ this.updateButtons(item);
+ this.clearStatusInfo();
+ }
+ },
+
+/**
+ * Import opml file into a feed account. Used by the Subscribe dialog and
+ * the Import wizard.
+ *
+ * @param nsILocalFile aFile - the opml file.
+ * @param nsIMsgIncomingServer aServer - the account server.
+ * @param func aCallback - callback function.
+ *
+ * @return bool - false if error.
+ */
+ importOPMLFile: function(aFile, aServer, aCallback)
+ {
+ if (aServer && (aServer instanceof Ci.nsIMsgIncomingServer))
+ this.mRSSServer = aServer;
+
+ if (!aFile || !this.mRSSServer || !aCallback)
+ return false;
+
+ let opmlDom, statusReport;
+ let stream = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+
+ // Read in file as raw bytes, so Expat can do the decoding for us.
+ try {
+ stream.init(aFile, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
+ let parser = new DOMParser();
+ opmlDom = parser.parseFromStream(stream, null, stream.available(),
+ "application/xml");
+ }
+ catch(e) {
+ statusReport = FeedUtils.strings.GetStringFromName(
+ "subscribe-errorOpeningFile");
+ Services.prompt.alert(window, null, statusReport);
+ return false;
+ }
+ finally {
+ stream.close();
+ }
+
+ let body = opmlDom ? opmlDom.querySelector("body") : null;
+
+ // Return if the OPML file is invalid or empty.
+ if (!body || !body.childElementCount ||
+ opmlDom.documentElement.tagName != "opml")
+ {
+ statusReport = FeedUtils.strings.formatStringFromName(
+ "subscribe-OPMLImportInvalidFile", [aFile.leafName], 1);
+ Services.prompt.alert(window, null, statusReport);
+ return false;
+ }
+
+ this.importOPMLOutlines(body, this.mRSSServer, aCallback);
+ return true;
+ },
+
+ importOPMLOutlines: function(aBody, aRSSServer, aCallback)
+ {
+ let win = this;
+ let rssServer = aRSSServer;
+ let callback = aCallback;
+ let outline, feedFolder;
+ let badTag = false;
+ let firstFeedInFolderQuickMode = null;
+ let lastFolder;
+ let feedsAdded = 0;
+ let rssOutlines = 0;
+ let folderOutlines = 0;
+
+ function processor(aParentNode, aParentFolder)
+ {
+ FeedUtils.log.trace("importOPMLOutlines: PROCESSOR tag:name:childs - " +
+ aParentNode.tagName + ":" +
+ aParentNode.getAttribute("text") + ":" +
+ aParentNode.childElementCount);
+ while (true)
+ {
+ if (aParentNode.tagName == "body" && !aParentNode.childElementCount)
+ {
+ // Finished.
+ let statusReport = win.importOPMLStatus(feedsAdded, rssOutlines);
+ callback(statusReport, lastFolder, win);
+ return;
+ }
+
+ outline = aParentNode.firstElementChild;
+ if (outline.tagName != "outline")
+ {
+ FeedUtils.log.info("importOPMLOutlines: skipping, node is not an " +
+ "<outline> - <" + outline.tagName + ">");
+ badTag = true;
+ break;
+ }
+
+ let outlineName = outline.getAttribute("text") ||
+ outline.getAttribute("title") ||
+ outline.getAttribute("xmlUrl");
+ let feedUrl, folderURI;
+
+ if (outline.getAttribute("type") == "rss")
+ {
+ // A feed outline.
+ feedUrl = outline.getAttribute("xmlUrl") || outline.getAttribute("url");
+ if (!feedUrl)
+ {
+ FeedUtils.log.info("importOPMLOutlines: skipping, type=rss <outline> " +
+ "has no url - " + outlineName);
+ break;
+ }
+
+ rssOutlines++;
+ feedFolder = aParentFolder;
+
+ if (FeedUtils.feedAlreadyExists(feedUrl, rssServer))
+ {
+ FeedUtils.log.info("importOPMLOutlines: feed already subscribed in account " +
+ rssServer.prettyName + ", url - " + feedUrl);
+ break;
+ }
+
+ if (aParentNode.tagName == "outline" &&
+ aParentNode.getAttribute("type") != "rss")
+ // Parent is a folder, already created.
+ folderURI = feedFolder.URI;
+ else
+ {
+ // Parent is not a folder outline, likely the <body> in a flat list.
+ // Create feed's folder with feed's name and account rootFolder as
+ // parent of feed's folder.
+ // NOTE: Assume a type=rss outline must be a leaf and is not a
+ // direct parent of another type=rss outline; such a structure
+ // may lead to unintended nesting and inaccurate counts.
+ }
+
+ // Create the feed.
+ let quickMode = outline.hasAttribute("fz:quickMode") ?
+ outline.getAttribute("fz:quickMode") == "true" :
+ rssServer.getBoolValue("quickMode");
+ let options = outline.getAttribute("fz:options");
+ options = options ? JSON.parse(options) : null;
+
+ if (firstFeedInFolderQuickMode === null)
+ // The summary/web page pref applies to all feeds in a folder,
+ // though it is a property of an individual feed. This can be
+ // set (and is obvious) in the subscribe dialog; ensure import
+ // doesn't leave mismatches if mismatched in the opml file.
+ firstFeedInFolderQuickMode = quickMode;
+ else
+ quickMode = firstFeedInFolderQuickMode;
+
+ let feedProperties = { feedName : outlineName,
+ feedLocation : feedUrl,
+ server : rssServer,
+ folderURI : folderURI,
+ quickMode : quickMode,
+ options : options };
+
+ FeedUtils.log.info("importOPMLOutlines: importing feed: name, url - "+
+ outlineName + ", " + feedUrl);
+
+ let feed = win.storeFeed(feedProperties);
+ if (outline.hasAttribute("htmlUrl"))
+ feed.link = outline.getAttribute("htmlUrl");
+
+ feed.createFolder();
+ if (!feed.folder)
+ {
+ // 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);
+ FeedUtils.log.info("importOPMLOutlines: skipping, error creating folder - '" +
+ feed.folderName + "' from outlineName - '" +
+ outlineName + "' in parent folder " +
+ aParentFolder.filePath.path);
+ badTag = true;
+ break;
+ }
+
+ // Add the feed to the databases.
+ FeedUtils.addFeed(feed);
+ // Feed correctly added.
+ feedsAdded++;
+ lastFolder = feed.folder;
+ }
+ else
+ {
+ // A folder outline. If a folder exists in the account structure at
+ // the same level as in the opml structure, feeds are placed into the
+ // existing folder.
+ let defaultName = FeedUtils.strings.GetStringFromName("ImportFeedsNew");
+ let folderName = FeedUtils.getSanitizedFolderName(aParentFolder,
+ outlineName,
+ defaultName,
+ false);
+ try {
+ feedFolder = aParentFolder.getChildNamed(folderName);
+ }
+ catch (ex) {
+ // Folder not found, create it.
+ FeedUtils.log.info("importOPMLOutlines: creating folder - '" +
+ folderName + "' from outlineName - '" +
+ outlineName + "' in parent folder " +
+ aParentFolder.filePath.path);
+ firstFeedInFolderQuickMode = null;
+ try {
+ feedFolder = aParentFolder.QueryInterface(Ci.nsIMsgLocalMailFolder).
+ createLocalSubfolder(folderName);
+ folderOutlines++;
+ }
+ catch (ex) {
+ // An error creating. Skip it.
+ FeedUtils.log.info("importOPMLOutlines: skipping, error creating folder - '" +
+ folderName + "' from outlineName - '" +
+ outlineName + "' in parent folder " +
+ aParentFolder.filePath.path);
+ let xfolder = aParentFolder.getChildNamed(folderName);
+ aParentFolder.propagateDelete(xfolder, true, null);
+ badTag = true;
+ break;
+ }
+ }
+ }
+ break;
+ }
+
+ if (!outline.childElementCount || badTag)
+ {
+ // Remove leaf nodes that are processed or bad tags from the opml dom,
+ // and go back to reparse. This method lets us use setTimeout to
+ // prevent UI hang, in situations of both deep and shallow trees.
+ // A yield/generator.next() method is fine for shallow trees, but not
+ // the true recursion required for deeper trees; both the shallow loop
+ // and the recurse should give it up.
+ outline.remove();
+ badTag = false;
+ outline = aBody;
+ feedFolder = rssServer.rootFolder;
+ }
+
+ setTimeout(function() {
+ processor(outline, feedFolder);
+ }, 0);
+ }
+
+ processor(aBody, rssServer.rootFolder);
+ },
+
+ importOPMLStatus: function(aFeedsAdded, aRssOutlines, aFolderOutlines)
+ {
+ let statusReport;
+ if (aRssOutlines > aFeedsAdded)
+ statusReport = FeedUtils.strings.formatStringFromName("subscribe-OPMLImportStatus",
+ [PluralForm.get(aFeedsAdded,
+ FeedUtils.strings.GetStringFromName("subscribe-OPMLImportUniqueFeeds"))
+ .replace("#1", aFeedsAdded),
+ PluralForm.get(aRssOutlines,
+ FeedUtils.strings.GetStringFromName("subscribe-OPMLImportFoundFeeds"))
+ .replace("#1", aRssOutlines)], 2);
+ else
+ statusReport = PluralForm.get(aFeedsAdded,
+ FeedUtils.strings.GetStringFromName("subscribe-OPMLImportFeedCount"))
+ .replace("#1", aFeedsAdded);
+
+ return statusReport;
+ },
+
+ importOPMLFinished: function(aStatusReport, aLastFolder, aWin)
+ {
+ if (aLastFolder)
+ {
+ aWin.selectFolder(aLastFolder, { select: false, newFolder: aLastFolder });
+ aWin.selectFolder(aLastFolder.parent);
+ }
+ aWin.mActionMode = null;
+ aWin.updateButtons(aWin.mView.currentItem);
+ aWin.clearStatusInfo();
+ aWin.updateStatusItem("statusText", aStatusReport);
+ }
+
+};
diff --git a/mailnews/extensions/newsblog/content/feed-subscriptions.xul b/mailnews/extensions/newsblog/content/feed-subscriptions.xul
new file mode 100644
index 000000000..d6f4ea18f
--- /dev/null
+++ b/mailnews/extensions/newsblog/content/feed-subscriptions.xul
@@ -0,0 +1,235 @@
+<?xml version="1.0"?>
+<!-- -*- Mode: Java; 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderPane.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger-newsblog/skin/feed-subscriptions.css" type="text/css"?>
+
+<!DOCTYPE window [
+ <!ENTITY % feedDTD SYSTEM "chrome://messenger-newsblog/locale/feed-subscriptions.dtd">
+ %feedDTD;
+ <!ENTITY % certDTD SYSTEM "chrome://pippki/locale/certManager.dtd">
+ %certDTD;
+]>
+
+<window id="subscriptionsDialog"
+ flex="1"
+ title="&feedSubscriptions.label;"
+ windowtype="Mail:News-BlogSubscriptions"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:nc="http://home.netscape.com/NC-rdf#"
+ persist="width height screenX screenY sizemode"
+ onload="FeedSubscriptions.onLoad();"
+ onclose="return FeedSubscriptions.onClose();"
+ onkeypress="FeedSubscriptions.onKeyPress(event);"
+ onmousedown="FeedSubscriptions.onMouseDown(event);">
+
+ <script type="application/javascript"
+ src="chrome://messenger/content/specialTabs.js"/>
+ <script type="application/javascript"
+ src="chrome://messenger-newsblog/content/feed-subscriptions.js"/>
+
+ <keyset id="extensionsKeys">
+ <key id="key_close"
+ key="&cmd.close.commandKey;"
+ modifiers="accel"
+ oncommand="window.close();"/>
+ <key id="key_close2"
+ keycode="VK_ESCAPE"
+ oncommand="window.close();"/>
+ </keyset>
+
+ <stringbundle id="bundle_newsblog"
+ src="chrome://messenger-newsblog/locale/newsblog.properties"/>
+ <stringbundle id="bundle_brand"
+ src="chrome://branding/locale/brand.properties"/>
+
+ <vbox flex="1" id="contentPane">
+ <hbox align="right">
+ <label id="learnMore"
+ class="text-link"
+ crop="end"
+ value="&learnMore.label;"
+ href="https://support.mozilla.org/kb/how-subscribe-news-feeds-and-blogs"/>
+ </hbox>
+
+ <tree id="rssSubscriptionsList"
+ treelines="true"
+ flex="1"
+ hidecolumnpicker="true"
+ onselect="FeedSubscriptions.onSelect();"
+ seltype="single">
+ <treecols>
+ <treecol id="folderNameCol"
+ flex="2"
+ primary="true"
+ hideheader="true"/>
+ </treecols>
+ <treechildren id="subscriptionChildren"
+ ondragstart="FeedSubscriptions.onDragStart(event);"
+ ondragover="FeedSubscriptions.onDragOver(event);"/>
+ </tree>
+
+ <hbox id="rssFeedInfoBox">
+ <vbox flex="1">
+ <grid flex="1">
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+ <rows>
+ <row>
+ <hbox align="right" valign="middle">
+ <label id="nameLabel"
+ accesskey="&feedTitle.accesskey;"
+ control="nameValue"
+ value="&feedTitle.label;"/>
+ </hbox>
+ <textbox id="nameValue"
+ clickSelectsAll="true"/>
+ </row>
+ <row>
+ <hbox align="right" valign="middle">
+ <label id="locationLabel"
+ accesskey="&feedLocation.accesskey;"
+ control="locationValue"
+ value="&feedLocation.label;"/>
+ </hbox>
+ <hbox>
+ <textbox id="locationValue"
+ flex="1"
+ class="uri-element"
+ placeholder="&feedLocation.placeholder;"
+ clickSelectsAll="true"
+ onfocus="FeedSubscriptions.setSummaryFocus();"
+ onblur="FeedSubscriptions.setSummaryFocus();"/>
+ <hbox align="center">
+ <label id="locationValidate"
+ collapsed="true"
+ class="text-link"
+ crop="end"
+ value="&locationValidate.label;"
+ onclick="FeedSubscriptions.checkValidation(event);"/>
+ </hbox>
+ </hbox>
+ </row>
+ <row>
+ <hbox align="right" valign="middle">
+ <label id="feedFolderLabel"
+ value="&feedFolder.label;"
+ accesskey="&feedFolder.accesskey;"
+ control="selectFolder"/>
+ </hbox>
+ <hbox>
+ <menulist id="selectFolder"
+ flex="1"
+ class="folderMenuItem"
+ hidden="true">
+ <menupopup id="selectFolderPopup"
+ class="menulist-menupopup"
+ type="folder"
+ mode="feeds"
+ showFileHereLabel="true"
+ showAccountsFileHere="true"
+ oncommand="FeedSubscriptions.setNewFolder(event)"/>
+ </menulist>
+ <textbox id="selectFolderValue"
+ flex="1"
+ readonly="true"
+ onkeypress="FeedSubscriptions.onClickSelectFolderValue(event)"
+ onclick="FeedSubscriptions.onClickSelectFolderValue(event)"/>
+ </hbox>
+ </row>
+ </rows>
+ </grid>
+ <checkbox id="quickMode"
+ accesskey="&quickMode.accesskey;"
+ label="&quickMode.label;"
+ oncommand="FeedSubscriptions.setSummary(this.checked)"/>
+ <checkbox id="autotagEnable"
+ accesskey="&autotagEnable.accesskey;"
+ label="&autotagEnable.label;"
+ oncommand="FeedSubscriptions.setCategoryPrefs(this)"/>
+ <hbox>
+ <checkbox id="autotagUsePrefix"
+ class="indent"
+ accesskey="&autotagUsePrefix.accesskey;"
+ label="&autotagUsePrefix.label;"
+ oncommand="FeedSubscriptions.setCategoryPrefs(this)"/>
+ <textbox id="autotagPrefix"
+ placeholder="&autoTagPrefix.placeholder;"
+ clickSelectsAll="true"/>
+ </hbox>
+ <separator class="thin"/>
+ </vbox>
+ </hbox>
+
+ <hbox id="statusContainerBox"
+ align="center"
+ valign="middle">
+ <vbox flex="1">
+ <description id="statusText"/>
+ </vbox>
+ <spacer flex="1"/>
+ <label id="validationText"
+ collapsed="true"
+ class="text-link"
+ crop="end"
+ value="&validateText.label;"
+ onclick="FeedSubscriptions.checkValidation(event);"/>
+ <button id="addCertException"
+ collapsed="true"
+ label="&certmgr.addException.label;"
+ accesskey="&certmgr.addException.accesskey;"
+ oncommand="FeedSubscriptions.addCertExceptionDialog();"/>
+ <progressmeter id="progressMeter"
+ collapsed="true"
+ mode="determined"
+ value="0"/>
+ </hbox>
+
+ <hbox align="end">
+ <hbox class="actionButtons" flex="1">
+ <button id="addFeed"
+ label="&button.addFeed.label;"
+ accesskey="&button.addFeed.accesskey;"
+ oncommand="FeedSubscriptions.addFeed();"/>
+
+ <button id="editFeed"
+ disabled="true"
+ label="&button.updateFeed.label;"
+ accesskey="&button.updateFeed.accesskey;"
+ oncommand="FeedSubscriptions.editFeed();"/>
+
+ <button id="removeFeed"
+ disabled="true"
+ label="&button.removeFeed.label;"
+ accesskey="&button.removeFeed.accesskey;"
+ oncommand="FeedSubscriptions.removeFeed(true);"/>
+
+ <button id="importOPML"
+ label="&button.importOPML.label;"
+ accesskey="&button.importOPML.accesskey;"
+ oncommand="FeedSubscriptions.importOPML();"/>
+
+ <button id="exportOPML"
+ label="&button.exportOPML.label;"
+ accesskey="&button.exportOPML.accesskey;"
+ tooltiptext="&button.exportOPML.tooltip;"
+ oncommand="FeedSubscriptions.exportOPML(event);"/>
+
+ <spacer flex="1"/>
+
+ <button id="close"
+ label="&button.close.label;"
+ icon="close"
+ oncommand="if (FeedSubscriptions.onClose()) window.close();"/>
+ </hbox>
+ </hbox>
+ </vbox>
+</window>
diff --git a/mailnews/extensions/newsblog/content/feedAccountWizard.js b/mailnews/extensions/newsblog/content/feedAccountWizard.js
new file mode 100644
index 000000000..a79da073a
--- /dev/null
+++ b/mailnews/extensions/newsblog/content/feedAccountWizard.js
@@ -0,0 +1,45 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+Components.utils.import("resource:///modules/FeedUtils.jsm");
+
+/* Feed account standalone wizard functions */
+var FeedAccountWizard = {
+ accountName: "",
+
+ accountSetupPageInit: function() {
+ this.accountSetupPageValidate();
+ },
+
+ accountSetupPageValidate: function() {
+ this.accountName = document.getElementById("prettyName").value.trim();
+ document.documentElement.canAdvance = this.accountName;
+ },
+
+ accountSetupPageUnload: function() {
+ return;
+ },
+
+ donePageInit: function() {
+ document.getElementById("account.name.text").value = this.accountName;
+ },
+
+ onCancel: function() {
+ return true;
+ },
+
+ onFinish: function() {
+ let account = FeedUtils.createRssAccount(this.accountName);
+ if ("gFolderTreeView" in window.opener.top)
+ // Opened from 3pane File->New or Appmenu New Message, or
+ // Account Central link.
+ window.opener.top.gFolderTreeView.selectFolder(account.incomingServer.rootMsgFolder);
+ else if ("selectServer" in window.opener)
+ // Opened from Account Settings.
+ window.opener.selectServer(account.incomingServer);
+
+ window.close();
+ }
+}
diff --git a/mailnews/extensions/newsblog/content/feedAccountWizard.xul b/mailnews/extensions/newsblog/content/feedAccountWizard.xul
new file mode 100644
index 000000000..0535fb237
--- /dev/null
+++ b/mailnews/extensions/newsblog/content/feedAccountWizard.xul
@@ -0,0 +1,79 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountWizard.css" type="text/css"?>
+
+<!DOCTYPE wizard [
+ <!ENTITY % accountDTD SYSTEM "chrome://messenger/locale/AccountWizard.dtd">
+ %accountDTD;
+ <!ENTITY % newsblogDTD SYSTEM "chrome://messenger-newsblog/locale/am-newsblog.dtd" >
+ %newsblogDTD;
+ <!ENTITY % imDTD SYSTEM "chrome://messenger/locale/imAccountWizard.dtd" >
+ %imDTD;
+]>
+
+<wizard id="FeedAccountWizard"
+ title="&feedWindowTitle.label;"
+ onwizardcancel="return FeedAccountWizard.onCancel();"
+ onwizardfinish="return FeedAccountWizard.onFinish();"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript"
+ src="chrome://messenger-newsblog/content/feedAccountWizard.js"/>
+
+ <!-- Account setup page : User gets a choice to enter a name for the account -->
+ <!-- Defaults : Feed account name -> default string -->
+ <wizardpage id="accountsetuppage"
+ pageid="accountsetuppage"
+ label="&accnameTitle.label;"
+ onpageshow="return FeedAccountWizard.accountSetupPageInit();"
+ onpageadvanced="return FeedAccountWizard.accountSetupPageUnload();">
+ <vbox flex="1">
+ <description>&accnameDesc.label;</description>
+ <separator class="thin"/>
+ <hbox align="center">
+ <label class="label"
+ value="&accnameLabel.label;"
+ accesskey="&accnameLabel.accesskey;"
+ control="prettyName"/>
+ <textbox id="prettyName"
+ flex="1"
+ value="&feeds.accountName;"
+ oninput="FeedAccountWizard.accountSetupPageValidate();"/>
+ </hbox>
+ </vbox>
+ </wizardpage>
+
+ <!-- Done page : Summarizes information collected to create a feed account -->
+ <wizardpage id="done"
+ pageid="done"
+ label="&accountSummaryTitle.label;"
+ onpageshow="return FeedAccountWizard.donePageInit();">
+ <vbox flex="1">
+ <description>&accountSummaryInfo.label;</description>
+ <separator class="thin"/>
+ <grid>
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+ <rows>
+ <row id="account.name"
+ align="center">
+ <label id="account.name.label"
+ class="label"
+ flex="1"
+ value="&accnameLabel.label;"/>
+ <label id="account.name.text"
+ class="label"/>
+ </row>
+ </rows>
+ </grid>
+ <separator/>
+ <spacer flex="1"/>
+ </vbox>
+ </wizardpage>
+
+</wizard>
diff --git a/mailnews/extensions/newsblog/content/newsblogOverlay.js b/mailnews/extensions/newsblog/content/newsblogOverlay.js
new file mode 100644
index 000000000..f7e08ec95
--- /dev/null
+++ b/mailnews/extensions/newsblog/content/newsblogOverlay.js
@@ -0,0 +1,363 @@
+/* -*- 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/. */
+
+Components.utils.import("resource:///modules/gloda/mimemsg.js");
+Components.utils.import("resource:///modules/mailServices.js");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+// This global is for SeaMonkey compatibility.
+var gShowFeedSummary;
+
+var FeedMessageHandler = {
+ gShowSummary: true,
+ gToggle: false,
+ kSelectOverrideWebPage: 0,
+ kSelectOverrideSummary: 1,
+ kSelectFeedDefault: 2,
+ kOpenWebPage: 0,
+ kOpenSummary: 1,
+ kOpenToggleInMessagePane: 2,
+ kOpenLoadInBrowser: 3,
+
+ /**
+ * How to load message on threadpane select.
+ */
+ get onSelectPref() {
+ return Services.prefs.getIntPref("rss.show.summary");
+ },
+
+ set onSelectPref(val) {
+ Services.prefs.setIntPref("rss.show.summary", val);
+ ReloadMessage();
+ },
+
+ /**
+ * Load web page on threadpane select.
+ */
+ get loadWebPageOnSelectPref() {
+ return Services.prefs.getIntPref("rss.message.loadWebPageOnSelect") ? true : false;
+ },
+
+ /**
+ * How to load message on open (enter/dbl click in threadpane, contextmenu).
+ */
+ get onOpenPref() {
+ return Services.prefs.getIntPref("rss.show.content-base");
+ },
+
+ set onOpenPref(val) {
+ Services.prefs.setIntPref("rss.show.content-base", val);
+ },
+
+ /**
+ * Determine if a message is a feed message. Prior to Tb15, a message had to
+ * be in an rss acount type folder. In Tb15 and later, a flag is set on the
+ * message itself upon initial store; the message can be moved to any folder.
+ *
+ * @param nsIMsgDBHdr aMsgHdr - the message.
+ *
+ * @return true if message is a feed, false if not.
+ */
+ isFeedMessage: function(aMsgHdr) {
+ return (aMsgHdr instanceof Components.interfaces.nsIMsgDBHdr) &&
+ ((aMsgHdr.flags & Components.interfaces.nsMsgMessageFlags.FeedMsg) ||
+ (aMsgHdr.folder && aMsgHdr.folder.server.type == "rss"));
+ },
+
+ /**
+ * Determine whether to show a feed message summary or load a web page in the
+ * message pane.
+ *
+ * @param nsIMsgDBHdr aMsgHdr - the message.
+ * @param bool aToggle - true if in toggle mode, false otherwise.
+ *
+ * @return true if summary is to be displayed, false if web page.
+ */
+ shouldShowSummary: function(aMsgHdr, aToggle) {
+ // Not a feed message, always show summary (the message).
+ if (!this.isFeedMessage(aMsgHdr))
+ return true;
+
+ // Notified of a summary reload when toggling, reset toggle and return.
+ if (!aToggle && this.gToggle)
+ return !(this.gToggle = false);
+
+ let showSummary = true;
+ this.gToggle = aToggle;
+
+ // Thunderbird 2 rss messages with 'Show article summary' not selected,
+ // ie message body constructed to show web page in an iframe, can't show
+ // a summary - notify user.
+ let browser = getBrowser();
+ let contentDoc = browser ? browser.contentDocument : null;
+ let rssIframe = contentDoc ? contentDoc.getElementById("_mailrssiframe") : null;
+ if (rssIframe) {
+ if (this.gToggle || this.onSelectPref == this.kSelectOverrideSummary)
+ this.gToggle = false;
+ return false;
+ }
+
+ if (aToggle)
+ // Toggle mode, flip value.
+ return gShowFeedSummary = this.gShowSummary = !this.gShowSummary;
+
+ let wintype = document.documentElement.getAttribute("windowtype");
+ let tabMail = document.getElementById("tabmail");
+ let messageTab = tabMail && tabMail.currentTabInfo.mode.type == "message";
+ let messageWindow = wintype == "mail:messageWindow";
+
+ switch (this.onSelectPref) {
+ case this.kSelectOverrideWebPage:
+ showSummary = false;
+ break;
+ case this.kSelectOverrideSummary:
+ showSummary = true
+ break;
+ case this.kSelectFeedDefault:
+ // Get quickmode per feed folder pref from feeds.rdf. If the feed
+ // message is not in a feed account folder (hence the folder is not in
+ // the feeds database), or FZ_QUICKMODE property is not found (possible
+ // in pre renovation urls), err on the side of showing the summary.
+ // For the former, toggle or global override is necessary; for the
+ // latter, a show summary checkbox toggle in Subscribe dialog will set
+ // one on the path to bliss.
+ let folder = aMsgHdr.folder, targetRes;
+ try {
+ targetRes = FeedUtils.getParentTargetForChildResource(
+ folder.URI, FeedUtils.FZ_QUICKMODE, folder.server);
+ }
+ catch (ex) {
+ // Not in a feed account folder or other error.
+ FeedUtils.log.info("FeedMessageHandler.shouldShowSummary: could not " +
+ "get summary pref for this folder");
+ }
+
+ showSummary = targetRes && targetRes.QueryInterface(Ci.nsIRDFLiteral).
+ Value == "false" ? false : true;
+ break;
+ }
+
+ gShowFeedSummary = this.gShowSummary = showSummary;
+
+ if (messageWindow || messageTab) {
+ // Message opened in either standalone window or tab, due to either
+ // message open pref (we are here only if the pref is 0 or 1) or
+ // contextmenu open.
+ switch (this.onOpenPref) {
+ case this.kOpenToggleInMessagePane:
+ // Opened by contextmenu, use the value derived above.
+ // XXX: allow a toggle via crtl?
+ break;
+ case this.kOpenWebPage:
+ showSummary = false;
+ break;
+ case this.kOpenSummary:
+ showSummary = true;
+ break;
+ }
+ }
+
+ // Auto load web page in browser on select, per pref; shouldShowSummary() is
+ // always called first to 1)test if feed, 2)get summary pref, so do it here.
+ if (this.loadWebPageOnSelectPref)
+ setTimeout(FeedMessageHandler.loadWebPage, 20, aMsgHdr, {browser:true});
+
+ return showSummary;
+ },
+
+ /**
+ * Load a web page for feed messages. Use MsgHdrToMimeMessage() to get
+ * the content-base url from the message headers. We cannot rely on
+ * currentHeaderData; it has not yet been streamed at our entry point in
+ * displayMessageChanged(), and in the case of a collapsed message pane it
+ * is not streamed.
+ *
+ * @param nsIMsgDBHdr aMessageHdr - the message.
+ * @param {obj} aWhere - name value=true pair, where name is in:
+ * 'messagepane', 'browser', 'tab', 'window'.
+ */
+ loadWebPage: function(aMessageHdr, aWhere) {
+ MsgHdrToMimeMessage(aMessageHdr, null, function(aMsgHdr, aMimeMsg) {
+ if (aMimeMsg && aMimeMsg.headers["content-base"] &&
+ aMimeMsg.headers["content-base"][0]) {
+ let url = aMimeMsg.headers["content-base"], uri;
+ try {
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ url = converter.ConvertToUnicode(url);
+ uri = Services.io.newURI(url, null, null);
+ url = uri.spec;
+ }
+ catch (ex) {
+ FeedUtils.log.info("FeedMessageHandler.loadWebPage: " +
+ "invalid Content-Base header url - " + url);
+ return;
+ }
+ if (aWhere.browser)
+ Components.classes["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Components.interfaces.nsIExternalProtocolService)
+ .loadURI(uri);
+ else if (aWhere.messagepane) {
+ let loadFlag = getBrowser().webNavigation.LOAD_FLAGS_NONE;
+ getBrowser().webNavigation.loadURI(url, loadFlag, null, null, null);
+ }
+ else if (aWhere.tab)
+ openContentTab(url, "tab", "^");
+ else if (aWhere.window)
+ openContentTab(url, "window", "^");
+ }
+ else
+ FeedUtils.log.info("FeedMessageHandler.loadWebPage: could not get " +
+ "Content-Base header url for this message");
+ });
+ },
+
+ /**
+ * Display summary or load web page for feed messages. Caller should already
+ * know if the message is a feed message.
+ *
+ * @param nsIMsgDBHdr aMsgHdr - the message.
+ * @param bool aShowSummary - true if summary is to be displayed, false if
+ * web page.
+ */
+ setContent: function(aMsgHdr, aShowSummary) {
+ if (aShowSummary) {
+ // Only here if toggling to summary in 3pane.
+ if (this.gToggle && gDBView && GetNumSelectedMessages() == 1)
+ ReloadMessage();
+ }
+ else {
+ let browser = getBrowser();
+ if (browser && browser.contentDocument && browser.contentDocument.body)
+ browser.contentDocument.body.hidden = true;
+ // If in a non rss folder, hide possible remote content bar on a web
+ // page load, as it doesn't apply.
+ if ("msgNotificationBar" in window)
+ gMessageNotificationBar.clearMsgNotifications();
+
+ this.loadWebPage(aMsgHdr, {messagepane:true});
+ this.gToggle = false;
+ }
+ }
+}
+
+function openSubscriptionsDialog(aFolder)
+{
+ // Check for an existing feed subscriptions window and focus it.
+ let subscriptionsWindow =
+ Services.wm.getMostRecentWindow("Mail:News-BlogSubscriptions");
+
+ if (subscriptionsWindow)
+ {
+ if (aFolder)
+ {
+ subscriptionsWindow.FeedSubscriptions.selectFolder(aFolder);
+ subscriptionsWindow.FeedSubscriptions.mView.treeBox.ensureRowIsVisible(
+ subscriptionsWindow.FeedSubscriptions.mView.selection.currentIndex);
+ }
+
+ subscriptionsWindow.focus();
+ }
+ else
+ {
+ window.openDialog("chrome://messenger-newsblog/content/feed-subscriptions.xul",
+ "", "centerscreen,chrome,dialog=no,resizable",
+ { folder: aFolder});
+ }
+}
+
+// Special case attempts to reply/forward/edit as new RSS articles. For
+// messages stored prior to Tb15, we are here only if the message's folder's
+// account server is rss and feed messages moved to other types will have their
+// summaries loaded, as viewing web pages only happened in an rss account.
+// The user may choose whether to load a summary or web page link by ensuring
+// the current feed message is being viewed as either a summary or web page.
+function openComposeWindowForRSSArticle(aMsgComposeWindow, aMsgHdr, aMessageUri,
+ aType, aFormat, aIdentity, aMsgWindow)
+{
+ // Ensure right content is handled for web pages in window/tab.
+ let tabmail = document.getElementById("tabmail");
+ let is3pane = tabmail && tabmail.selectedTab && tabmail.selectedTab.mode ?
+ tabmail.selectedTab.mode.type == "folder" : false;
+ let showingwebpage = ("FeedMessageHandler" in window) && !is3pane &&
+ FeedMessageHandler.onOpenPref == FeedMessageHandler.kOpenWebPage;
+
+ if (gShowFeedSummary && !showingwebpage)
+ {
+ // The user is viewing the summary.
+ MailServices.compose.OpenComposeWindow(aMsgComposeWindow, aMsgHdr, aMessageUri,
+ aType, aFormat, aIdentity, aMsgWindow);
+
+ }
+ else
+ {
+ // Set up the compose message and get the feed message's web page link.
+ let Cc = Components.classes;
+ let Ci = Components.interfaces;
+ let msgHdr = aMsgHdr;
+ let type = aType;
+ let msgComposeType = Ci.nsIMsgCompType;
+ let subject = msgHdr.mime2DecodedSubject;
+ let fwdPrefix = Services.prefs.getCharPref("mail.forward_subject_prefix");
+ fwdPrefix = fwdPrefix ? fwdPrefix + ": " : "";
+
+ let params = Cc["@mozilla.org/messengercompose/composeparams;1"]
+ .createInstance(Ci.nsIMsgComposeParams);
+
+ let composeFields = Cc["@mozilla.org/messengercompose/composefields;1"]
+ .createInstance(Ci.nsIMsgCompFields);
+
+ if (type == msgComposeType.Reply ||
+ type == msgComposeType.ReplyAll ||
+ type == msgComposeType.ReplyToSender ||
+ type == msgComposeType.ReplyToGroup ||
+ type == msgComposeType.ReplyToSenderAndGroup)
+ {
+ subject = "Re: " + subject;
+ }
+ else if (type == msgComposeType.ForwardInline ||
+ type == msgComposeType.ForwardAsAttachment)
+ {
+ subject = fwdPrefix + subject;
+ }
+
+ params.composeFields = composeFields;
+ params.composeFields.subject = subject;
+ params.composeFields.characterSet = msgHdr.Charset;
+ params.composeFields.body = "";
+ params.bodyIsLink = false;
+ params.identity = aIdentity;
+
+ try
+ {
+ // The feed's web page url is stored in the Content-Base header.
+ MsgHdrToMimeMessage(msgHdr, null, function(aMsgHdr, aMimeMsg) {
+ if (aMimeMsg && aMimeMsg.headers["content-base"] &&
+ aMimeMsg.headers["content-base"][0])
+ {
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let url = converter.ConvertToUnicode(aMimeMsg.headers["content-base"]);
+ params.composeFields.body = url;
+ params.bodyIsLink = true;
+ MailServices.compose.OpenComposeWindowWithParams(null, params);
+ }
+ else
+ // No content-base url, use the summary.
+ MailServices.compose.OpenComposeWindow(aMsgComposeWindow, aMsgHdr, aMessageUri,
+ aType, aFormat, aIdentity, aMsgWindow);
+
+ }, false, {saneBodySize: true});
+ }
+ catch (ex)
+ {
+ // Error getting header, use the summary.
+ MailServices.compose.OpenComposeWindow(aMsgComposeWindow, aMsgHdr, aMessageUri,
+ aType, aFormat, aIdentity, aMsgWindow);
+ }
+ }
+}