mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-29 03:47:31 +09:00
1609 lines
58 KiB
JavaScript
1609 lines
58 KiB
JavaScript
/* -*- 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
|
|
// <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);
|
|
});
|