Bug #252859 --> Add support for dropping RSS urls onto RSS servers and folders to automatically subscribe to the feed.

Bug #252860 --> Add error reporting to alert the user when the feed url could not be downloaded or if the feed url wasn't an rss type we could understand.

sr=bienvenu
This commit is contained in:
scott%scott-macgregor.org 2004-07-24 19:58:43 +00:00
parent 8ad0b5d01a
commit 0f6bca050a
7 changed files with 190 additions and 92 deletions

View File

@ -14,6 +14,12 @@ var serializer =
.classes["@mozilla.org/xmlextras/xmlserializer;1"]
.createInstance(Components.interfaces.nsIDOMSerializer);
// error codes used to inform the consumer about attempts to download a feed
const kNewsBlogSuccess = 0;
const kNewsBlogInvalidFeed = 1; // usually means there was an error trying to parse the feed...
const kNewsBlogRequestFailure = 2; // generic networking failure when trying to download the feed.
// Hash of feeds being downloaded, indexed by URL, so the load event listener
// can access the Feed objects after it finishes downloading the feed files.
var gFzFeedCache = new Object();
@ -57,19 +63,27 @@ Feed.prototype.name getter = function() {
}
Feed.prototype.download = function(parseItems, aCallback) {
this.downloadCallback = aCallback; // may be null
// Whether or not to parse items when downloading and parsing the feed.
// Defaults to true, but setting to false is useful for obtaining
// just the title of the feed when the user subscribes to it.
this.parseItems = parseItems == null ? true : parseItems ? true : false;
// Before we do anything...make sure the url is an http url. This is just a sanity check
// so we don't try opening mailto urls, imap urls, etc. that the user may have tried to subscribe to
// as an rss feed..
var uri = Components.classes["@mozilla.org/network/standard-url;1"].
createInstance(Components.interfaces.nsIURI);
uri.spec = this.url;
if (!uri.schemeIs("http"))
return this.onParseError(this); // simulate an invalid feed error
this.request = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Components.interfaces.nsIXMLHttpRequest);
this.request.onprogress = Feed.onProgress; // must be set before calling .open
this.request.open("GET", this.url, true);
this.downloadCallback = aCallback; // may be null
this.request.overrideMimeType("text/xml");
this.request.onload = Feed.onDownloaded;
this.request.onerror = Feed.onDownloadError;
@ -100,17 +114,13 @@ Feed.onProgress = function(event) {
}
Feed.onDownloadError = function(event) {
// XXX add error message if available and notify the user?
var request = event.target;
var url = request.channel.originalURI.spec;
var feed = gFzFeedCache[url];
if (feed)
{
debug(feed.title + " download failed");
if (feed.downloadCallback)
feed.downloaded(feed, false);
}
throw("error downloading feed " + url);
if (feed.downloadCallback)
feed.downloadCallback.downloaded(feed, kNewsBlogRequestFailure);
}
Feed.prototype.onParseError = function(feed) {
if (feed && feed.downloadCallback)
feed.downloadCallback.downloaded(feed, kNewsBlogInvalidFeed);
}
Feed.prototype.url getter = function() {
@ -159,8 +169,7 @@ Feed.prototype.parse = function() {
debug("parsing feed " + this.url);
if (!this.request.responseText) {
throw("error parsing feed " + this.url + ": no data");
return;
return this.onParseError(this);
}
else if (this.request.responseText.search(/="http:\/\/purl\.org\/rss\/1\.0\/"/) != -1) {
debug(this.url + " is an RSS 1.x (RDF-based) feed");
@ -192,12 +201,12 @@ Feed.prototype.parse = function() {
Feed.prototype.parseAsRSS2 = function() {
if (!this.request.responseXML || !(this.request.responseXML instanceof Components.interfaces.nsIDOMXMLDocument))
throw("error parsing RSS 2.0 feed " + this.url + ": data not parsed into XMLDocument object");
return this.onParseError(this);
// Get the first channel (assuming there is only one per RSS File).
var channel = this.request.responseXML.getElementsByTagName("channel")[0];
if (!channel)
throw("error parsing RSS 2.0 feed " + this.url + ": channel element missing");
return this.onParseError(this);
this.title = this.title || getNodeValue(channel.getElementsByTagName("title")[0]);
this.description = getNodeValue(channel.getElementsByTagName("description")[0]);
@ -311,12 +320,12 @@ Feed.prototype.parseAsRSS1 = function() {
Feed.prototype.parseAsAtom = function() {
if (!this.request.responseXML || !(this.request.responseXML instanceof Components.interfaces.nsIDOMXMLDocument))
throw("error parsing Atom feed " + this.url + ": data not parsed into XMLDocument object");
return this.onParseError(this);
// Get the first channel (assuming there is only one per Atom File).
var channel = this.request.responseXML.getElementsByTagName("feed")[0];
if (!channel)
throw("channel missing from Atom feed " + request.channel.name);
return this.onParseError(this);
this.title = this.title || getNodeValue(channel.getElementsByTagName("title")[0]);
this.description = getNodeValue(channel.getElementsByTagName("tagline")[0]);
@ -443,6 +452,7 @@ Feed.prototype.removeInvalidItems = function() {
var gItemsToStore;
var gItemsToStoreIndex = 0;
var gStoreItemsTimer;
// gets the next item from gItemsToStore and forces that item to be stored
// to the folder. If more items are left to be stored, fires a timer for the next one.
@ -463,20 +473,16 @@ function storeNextItem()
if (gItemsToStoreIndex < gItemsToStore.length)
{
if ('setTimeout' in this)
setTimeout(storeNextItem, 50); // fire off a timer for the next item to store
else
{
debug('set timeout is not defined if this call originated from newsblog.js\n');
storeNextItem();
}
if (!gStoreItemsTimer)
gStoreItemsTimer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
gStoreItemsTimer.initWithCallback(storeNextItemTimerCallback, 50, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
}
else
{
item.feed.removeInvalidItems();
if (item.feed.downloadCallback)
item.feed.downloadCallback.downloaded(item.feed, true);
item.feed.downloadCallback.downloaded(item.feed, kNewsBlogSuccess);
item.feed.request = null; // force the xml http request to go away. This helps reduce some
// nasty assertions on shut down of all things.
@ -485,3 +491,17 @@ function storeNextItem()
gItemsToStoreIndex = 0;
}
}
var storeNextItemTimerCallback = {
notify: function(aTimer) {
storeNextItem();
},
QueryInterface: function(aIID) {
if (aIID.equals(Components.interfaces.nsITimerCallback) || aIID.equals(Components.interfaces.nsISupports))
return this;
Components.returnCode = Components.results.NS_ERROR_NO_INTERFACE;
return null;
}
}

View File

@ -34,7 +34,6 @@
*
* ***** END LICENSE BLOCK ***** */
var kFeedUrlDelimiter = '|'; // the delimiter used to delimit feed urls in the msg folder database "feedUrl" property
var gRSSServer = null;
function doLoad() {
@ -74,10 +73,10 @@ function clearStatusInfo()
}
var feedDownloadCallback = {
downloaded: function(feed, aSuccess)
downloaded: function(feed, aErrorCode)
{
// feed is null if our attempt to parse the feed failed
if (aSuccess)
if (aErrorCode == kNewsBlogSuccess)
{
updateStatusItem('progressMeter', 100);
@ -91,10 +90,10 @@ var feedDownloadCallback = {
// it also flushes the subscription datasource
addFeed(feed.url, feed.name, null, folder);
}
else
{
// Add some code to alert the user that the feed was not something we could understand...
}
else if (aErrorCode == kNewsBlogInvalidFeed) // the feed was bad...
window.alert(document.getElementById('bundle_newsblog').getFormattedString('newsblog-invalidFeed', [feed.url]));
else // we never even downloaded the feed...(kNewsBlogRequestFailure)
window.alert(document.getElementById('bundle_newsblog').getFormattedString('newsblog-networkError', [feed.url]));
// our operation is done...clear out the status text and progressmeter
setTimeout(clearStatusInfo, 1000);
@ -116,23 +115,6 @@ var feedDownloadCallback = {
},
}
// updates the "feedUrl" property in the message database for the folder in question.
function updateFolderFeedUrl(aFolder, aFeedUrl, aRemoveUrl)
{
var msgdb = aFolder.QueryInterface(Components.interfaces.nsIMsgFolder).getMsgDatabase(null);
var folderInfo = msgdb.dBFolderInfo;
var oldFeedUrl = folderInfo.getCharPtrProperty("feedUrl");
if (aRemoveUrl)
{
// remove our feed url string from the list of feed urls
var newFeedUrl = oldFeedUrl.replace(kFeedUrlDelimiter + aFeedUrl, "");
folderInfo.setCharPtrProperty("feedUrl", newFeedUrl);
}
else
folderInfo.setCharPtrProperty("feedUrl", oldFeedUrl + kFeedUrlDelimiter + aFeedUrl);
}
function doAdd() {
var userAddedFeed = false;
var feedProperties = { feedName: "", feedLocation: "", serverURI: gRSSServer.serverURI, folderURI: "", result: userAddedFeed};

View File

@ -88,6 +88,25 @@ function addFeed(url, title, quickMode, destFolder) {
ds.Flush();
}
// updates the "feedUrl" property in the message database for the folder in question.
var kFeedUrlDelimiter = '|'; // the delimiter used to delimit feed urls in the msg folder database "feedUrl" property
function updateFolderFeedUrl(aFolder, aFeedUrl, aRemoveUrl)
{
var msgdb = aFolder.QueryInterface(Components.interfaces.nsIMsgFolder).getMsgDatabase(null);
var folderInfo = msgdb.dBFolderInfo;
var oldFeedUrl = folderInfo.getCharPtrProperty("feedUrl");
if (aRemoveUrl)
{
// remove our feed url string from the list of feed urls
var newFeedUrl = oldFeedUrl.replace(kFeedUrlDelimiter + aFeedUrl, "");
folderInfo.setCharPtrProperty("feedUrl", newFeedUrl);
}
else
folderInfo.setCharPtrProperty("feedUrl", oldFeedUrl + kFeedUrlDelimiter + aFeedUrl);
}
function getNodeValue(node) {
if (node && node.textContent)

View File

@ -51,7 +51,7 @@ var nsNewsBlogFeedDownloader =
var rdf = Components.classes["@mozilla.org/rdf/rdf-service;1"]
.getService(Components.interfaces.nsIRDFService);
progressNotifier.init(aMsgWindow.statusFeedback);
progressNotifier.init(aMsgWindow.statusFeedback, false);
var index = 0;
for (url in feedUrlArray)
@ -68,6 +68,23 @@ var nsNewsBlogFeedDownloader =
}
},
subscribeToFeed: function(aUrl, aFolder, aMsgWindow)
{
if (!gExternalScriptsLoaded)
loadScripts();
var rdf = Components.classes["@mozilla.org/rdf/rdf-service;1"]
.getService(Components.interfaces.nsIRDFService);
var itemResource = rdf.GetResource(aUrl);
var feed = new Feed(itemResource);
feed.server = aFolder.server;
if (!aFolder.server.isServer) // if the root server, create a new folder for the feed
feed.folder = aFolder; // user must want us to add this subscription url to an existing RSS folder.
progressNotifier.init(aMsgWindow.statusFeedback, true);
feed.download(true, progressNotifier);
},
QueryInterface: function(aIID)
{
if (aIID.equals(Components.interfaces.nsINewsBlogFeedDownloader) ||
@ -222,33 +239,52 @@ function loadScripts()
var gNumPendingFeedDownloads = 0;
var progressNotifier = {
mSubscribeMode: false,
mStatusFeedback: null,
mFeeds: new Array,
init: function(aStatusFeedback)
init: function(aStatusFeedback, aSubscribeMode)
{
if (!gNumPendingFeedDownloads) // if we aren't already in the middle of downloading feed items...
{
this.mStatusFeedback = aStatusFeedback;
this.mSubscribeMode = aSubscribeMode;
this.mStatusFeedback.startMeteors();
this.mStatusFeedback.showStatusString(GetString('newsblog-getNewMailCheck'));
this.mStatusFeedback.showStatusString(aSubscribeMode ? GetNewsBlogStringBundle().GetStringFromName('subscribe-validating')
: GetNewsBlogStringBundle().GetStringFromName('newsblog-getNewMailCheck'));
}
},
downloaded: function(feed)
downloaded: function(feed, aErrorCode)
{
if (this.mSubscribeMode && aErrorCode == kNewsBlogSuccess)
{
// if we get here...we should always have a folder by now...either
// in feed.folder or FeedItems created the folder for us....
var folder = feed.folder ? feed.folder : feed.server.rootMsgFolder.getChildNamed(feed.name);
updateFolderFeedUrl(folder, feed.url, false);
addFeed(feed.url, feed.name, null, folder); // add feed just adds the feed to the subscription UI and flushes the datasource
}
else if (aErrorCode == kNewsBlogInvalidFeed)
this.mStatusFeedback.showStatusString(GetNewsBlogStringBundle().formatStringFromName("newsblog-invalidFeed",
[feed.url], 1));
else if (aErrorCode == kNewsBlogRequestFailure)
this.mStatusFeedback.showStatusString(GetNewsBlogStringBundle().formatStringFromName("newsblog-networkError",
[feed.url], 1));
this.mStatusFeedback.stopMeteors();
gNumPendingFeedDownloads--;
if (!gNumPendingFeedDownloads)
{
this.mFeeds = new Array;
// no more pending actions...clear the status bar text...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.
this.mSubscribeMode = false;
this.mStatusFeedback.showStatusString("");
// 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.
if (aErrorCode == kNewsBlogSuccess) // don't clear the status text if we just dumped an error to the status bar!
this.mStatusFeedback.showStatusString("");
}
},
@ -259,6 +295,12 @@ var progressNotifier = {
{
// we currently don't do anything here. Eventually we may add
// status text about the number of new feed articles received.
if (this.mSubscribeMode) // if we are subscribing to a feed, show feed download progress
{
this.mStatusFeedback.showStatusString(GetNewsBlogStringBundle().formatStringFromName("subscribe-fetchingFeedItems", [aCurrentFeedItems, aMaxFeedItems], 2));
this.onProgress(feed, aCurrentFeedItems, aMaxFeedItems);
}
},
onProgress: function(feed, aProgress, aProgressMax)
@ -292,10 +334,10 @@ var progressNotifier = {
}
}
function GetString(name)
function GetNewsBlogStringBundle(name)
{
var strBundleService = Components.classes["@mozilla.org/intl/stringbundle;1"].getService();
strBundleService = strBundleService.QueryInterface(Components.interfaces.nsIStringBundleService);
var strBundle = strBundleService.createBundle("chrome://messenger-newsblog/locale/newsblog.properties");
return strBundle.GetStringFromName(name);
return strBundle;
}

View File

@ -1,6 +1,6 @@
# Status strings used in the subscribe dialog
subscribe-validating=Verifying the feed...
subscribe-validating=Verifying the RSS feed...
# when downloading new feed items from the subscribe dialog.
# LOCALIZATION NOTE: Do not translate %d in the following line.
@ -8,4 +8,6 @@ subscribe-validating=Verifying the feed...
# the second %S will receive the total number of messages
subscribe-fetchingFeedItems=Downloading feed articles (%S of %S)
newsblog-invalidFeed=%S is not a valid RSS feed.
newsblog-networkError=%S could not be found. Please check the name and try again.
newsblog-getNewMailCheck=Checking RSS feeds for new items

View File

@ -69,6 +69,7 @@ function CanDropOnFolderTree(index, orientation)
trans.addDataFlavor("text/x-moz-message");
trans.addDataFlavor("text/x-moz-folder");
trans.addDataFlavor("text/x-moz-url");
var folderTree = GetFolderTree();
var targetResource = GetFolderResource(folderTree, index);
@ -121,33 +122,43 @@ function CanDropOnFolderTree(index, orientation)
if (hdr.folder == targetFolder)
return false;
break;
} else if (dataFlavor.value == "text/x-moz-folder") {
// we should only get here if we are dragging and dropping folders
dragFolder = true;
sourceResource = RDF.GetResource(sourceUri);
var sourceFolder = sourceResource.QueryInterface(Components.interfaces.nsIMsgFolder);
sourceServer = sourceFolder.server;
if (targetUri == sourceUri)
return false;
//don't allow drop on different imap servers.
if (sourceServer != targetServer && targetServer.type == "imap")
return false;
//don't allow immediate child to be dropped to it's parent
if (targetFolder.URI == sourceFolder.parent.URI)
{
debugDump(targetFolder.URI + "\n");
debugDump(sourceFolder.parent.URI + "\n");
return false;
}
var isAncestor = sourceFolder.isAncestorOf(targetFolder);
// don't allow parent to be dropped on its ancestors
if (isAncestor)
return false;
} else if (dataFlavor.value == "text/x-moz-url") {
// eventually check to make sure this is an http url before doing anything else...
var uri = Components.classes["@mozilla.org/network/standard-url;1"].
createInstance(Components.interfaces.nsIURI);
var url = sourceUri.split("\n")[0];
uri.spec = url;
if (uri.schemeIs("http") && targetServer && targetServer.type == 'rss')
return true;
}
// we should only get here if we are dragging and dropping folders
dragFolder = true;
sourceResource = RDF.GetResource(sourceUri);
var sourceFolder = sourceResource.QueryInterface(Components.interfaces.nsIMsgFolder);
sourceServer = sourceFolder.server;
if (targetUri == sourceUri)
return false;
//don't allow drop on different imap servers.
if (sourceServer != targetServer && targetServer.type == "imap")
return false;
//don't allow immediate child to be dropped to it's parent
if (targetFolder.URI == sourceFolder.parent.URI)
{
debugDump(targetFolder.URI + "\n");
debugDump(sourceFolder.parent.URI + "\n");
return false;
}
var isAncestor = sourceFolder.isAncestorOf(targetFolder);
// don't allow parent to be dropped on its ancestors
if (isAncestor)
return false;
}
if (dragFolder)
@ -198,6 +209,8 @@ function DropOnFolderTree(row, orientation)
var folderTree = GetFolderTree();
var targetResource = GetFolderResource(folderTree, row);
var targetFolder = targetResource.QueryInterface(Components.interfaces.nsIMsgFolder);
var targetServer = targetFolder.server;
var targetUri = targetResource.Value;
debugDump("***targetUri = " + targetUri + "\n");
@ -209,6 +222,7 @@ function DropOnFolderTree(row, orientation)
var trans = Components.classes["@mozilla.org/widget/transferable;1"].createInstance(Components.interfaces.nsITransferable);
trans.addDataFlavor("text/x-moz-message");
trans.addDataFlavor("text/x-moz-folder");
trans.addDataFlavor("text/x-moz-url");
var list = Components.classes["@mozilla.org/supports-array;1"].createInstance(Components.interfaces.nsISupportsArray);
@ -248,6 +262,24 @@ function DropOnFolderTree(row, orientation)
}
else if (flavor.value == "text/x-moz-message")
dropMessage = true;
else if (flavor.value == "text/x-moz-url")
{
var uri = Components.classes["@mozilla.org/network/standard-url;1"].
createInstance(Components.interfaces.nsIURI);
var url = sourceUri.split("\n")[0];
uri.spec = url;
if (uri.schemeIs("http") && targetServer && targetServer.type == 'rss')
{
var rssService = Components.classes["@mozilla.org/newsblog-feed-downloader;1"].getService().
QueryInterface(Components.interfaces.nsINewsBlogFeedDownloader);
if (rssService)
rssService.subscribeToFeed(url, targetFolder, msgWindow);
return true;
}
else
return false;
}
}
else {
if (!dropMessage)
@ -273,9 +305,6 @@ function DropOnFolderTree(row, orientation)
var isSourceNews = false;
isSourceNews = isNewsURI(sourceUri);
var targetFolder = targetResource.QueryInterface(Components.interfaces.nsIMsgFolder);
var targetServer = targetFolder.server;
if (dropMessage) {
var sourceMsgHdr = list.GetElementAt(0).QueryInterface(Components.interfaces.nsIMsgDBHdr);

View File

@ -46,5 +46,9 @@ interface nsINewsBlogFeedDownloader : nsISupports
void downloadFeed(in string aUrl, in nsIMsgFolder aFolder,
in boolean aQuickMode, in wstring aTitle,
in nsIUrlListener aUrlListener, in nsIMsgWindow aMsgWindow);
/* A convient method to subscribe to feeds without going through the subscribe UI
used by drag and drop */
void subscribeToFeed(in string aUrl, in nsIMsgFolder aFolder, in nsIMsgWindow aMsgWindow);
};