/* -*- 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_PUBLISHER() { return this.rdf.GetResource(this.DC_NS + "publisher"); },
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
// .
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 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: '\n' +
'\n' +
' \n' +
' \n' +
' \n' +
' \n' +
' \n' +
' \n' +
'\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: '\n' +
'\n' +
'\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);
});