зеркало из https://github.com/mozilla/pjs.git
483 строки
16 KiB
JavaScript
Executable File
483 строки
16 KiB
JavaScript
Executable File
# -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
|
|
# ***** BEGIN LICENSE BLOCK *****
|
|
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
#
|
|
# The contents of this file are subject to the Mozilla Public License Version
|
|
# 1.1 (the "License"); you may not use this file except in compliance with
|
|
# the License. You may obtain a copy of the License at
|
|
# http://www.mozilla.org/MPL/
|
|
#
|
|
# Software distributed under the License is distributed on an "AS IS" basis,
|
|
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
# for the specific language governing rights and limitations under the
|
|
# License.
|
|
#
|
|
# The Original Code is the RSS Parsing Engine
|
|
#
|
|
# The Initial Developer of the Original Code is
|
|
# The Mozilla Foundation.
|
|
# Portions created by the Initial Developer are Copyright (C) 2004
|
|
# the Initial Developer. All Rights Reserved.
|
|
#
|
|
# Contributor(s):
|
|
#
|
|
# Alternatively, the contents of this file may be used under the terms of
|
|
# either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
# in which case the provisions of the GPL or the LGPL are applicable instead
|
|
# of those above. If you wish to allow use of your version of this file only
|
|
# under the terms of either the GPL or the LGPL, and not to allow others to
|
|
# use your version of this file under the terms of the MPL, indicate your
|
|
# decision by deleting the provisions above and replace them with the notice
|
|
# and other provisions required by the GPL or the LGPL. If you do not delete
|
|
# the provisions above, a recipient may use your version of this file under
|
|
# the terms of any one of the MPL, the GPL or the LGPL.
|
|
#
|
|
# ***** END LICENSE BLOCK ***** */
|
|
|
|
|
|
// 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.
|
|
const kNewsBlogFeedIsBusy = 3;
|
|
const kNewsBlogNoNewItems = 4; // there are no new articles for this feed
|
|
|
|
// Cache for all of the feeds currently being downloaded, indexed by URL, so the load event listener
|
|
// can access the Feed objects after it finishes downloading the feed.
|
|
var FeedCache =
|
|
{
|
|
mFeeds: new Array(),
|
|
|
|
putFeed: function (aFeed)
|
|
{
|
|
this.mFeeds[this.normalizeHost(aFeed.url)] = aFeed;
|
|
},
|
|
|
|
getFeed: function (aUrl)
|
|
{
|
|
return this.mFeeds[this.normalizeHost(aUrl)];
|
|
},
|
|
|
|
removeFeed: function (aUrl)
|
|
{
|
|
delete this.mFeeds[this.normalizeHost(aUrl)];
|
|
},
|
|
|
|
normalizeHost: function (aUrl)
|
|
{
|
|
var ioService = Components.classes["@mozilla.org/network/io-service;1"].
|
|
getService(Components.interfaces.nsIIOService);
|
|
var normalizedUrl = ioService.newURI(aUrl, null, null);
|
|
normalizedUrl.host = normalizedUrl.host.toLowerCase();
|
|
return normalizedUrl.spec;
|
|
}
|
|
};
|
|
|
|
function Feed(aResource, aRSSServer)
|
|
{
|
|
this.resource = aResource.QueryInterface(Components.interfaces.nsIRDFResource);
|
|
this.server = aRSSServer;
|
|
}
|
|
|
|
Feed.prototype =
|
|
{
|
|
description: null,
|
|
author: null,
|
|
request: null,
|
|
server: null,
|
|
downloadCallback: null,
|
|
resource: null,
|
|
items: new Array(),
|
|
mFolder: null,
|
|
|
|
get folder()
|
|
{
|
|
if (!this.mFolder)
|
|
{
|
|
try
|
|
{
|
|
this.mFolder = this.server.rootMsgFolder.getChildNamed(this.name);
|
|
} catch (ex) {}
|
|
}
|
|
|
|
return this.mFolder;
|
|
},
|
|
|
|
set folder (aFolder)
|
|
{
|
|
this.mFolder = aFolder;
|
|
},
|
|
|
|
get name()
|
|
{
|
|
var name = this.title || this.description || this.url;
|
|
if (!name)
|
|
throw("couldn't compute feed name, as feed has no title, description, or URL.");
|
|
|
|
// Make sure the feed name doesn't have any line breaks, since we're going
|
|
// to use it as the name of the folder in the filesystem. This may not
|
|
// be necessary, since Mozilla's mail code seems to handle other forbidden
|
|
// characters in filenames and can probably handle these as well.
|
|
name = name.replace(/[\n\r\t]+/g, " ");
|
|
|
|
// Make sure the feed doesn't end in a period to work around bug 117840.
|
|
name = name.replace(/\.+$/, "");
|
|
|
|
return name;
|
|
},
|
|
|
|
download: function(aParseItems, 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 = aParseItems == null ? true : aParseItems ? true : false;
|
|
|
|
// Before we do anything...make sure the url is an http url. This is just a sanity check
|
|
// so we don't try opening mailto urls, imap urls, etc. that the user may have tried to subscribe to
|
|
// as an rss feed..
|
|
var uri = Components.classes["@mozilla.org/network/standard-url;1"].
|
|
createInstance(Components.interfaces.nsIURI);
|
|
uri.spec = this.url;
|
|
if (!(uri.schemeIs("http") || uri.schemeIs("https")))
|
|
{
|
|
this.onParseError(this); // simulate an invalid feed error
|
|
return;
|
|
}
|
|
|
|
// Before we try to download the feed, make sure we aren't already processing the feed
|
|
// by looking up the url in our feed cache
|
|
if (FeedCache.getFeed(this.url))
|
|
{
|
|
if (this.downloadCallback)
|
|
this.downloadCallback.downloaded(this, kNewsBlogFeedIsBusy);
|
|
return ; // don't do anything, the feed is already in use
|
|
}
|
|
|
|
this.request = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
|
.createInstance(Components.interfaces.nsIXMLHttpRequest);
|
|
this.request.onprogress = this.onProgress; // must be set before calling .open
|
|
this.request.open("GET", this.url, true);
|
|
|
|
var lastModified = this.lastModified;
|
|
if (lastModified)
|
|
this.request.setRequestHeader("If-Modified-Since", lastModified);
|
|
|
|
this.request.overrideMimeType("text/xml");
|
|
this.request.onload = this.onDownloaded;
|
|
this.request.onerror = this.onDownloadError;
|
|
FeedCache.putFeed(this);
|
|
this.request.send(null);
|
|
},
|
|
|
|
onDownloaded: function(aEvent)
|
|
{
|
|
var request = aEvent.target;
|
|
var url = request.channel.originalURI.spec;
|
|
debug(url + " downloaded");
|
|
var feed = FeedCache.getFeed(url);
|
|
if (!feed)
|
|
throw("error after downloading " + url + ": couldn't retrieve feed from request");
|
|
|
|
// if the request has a Last-Modified header on it, then go ahead and remember
|
|
// that as a property on the feed so we can use it when making future requests.
|
|
var lastModifiedHeader = request.getResponseHeader('Last-Modified');
|
|
if (lastModifiedHeader)
|
|
this.lastModified = lastModifiedHeader;
|
|
|
|
feed.parse(); // parse will asynchronously call the download callback when it is done
|
|
},
|
|
|
|
onProgress: function(aEvent)
|
|
{
|
|
var request = aEvent.target;
|
|
var url = request.channel.originalURI.spec;
|
|
var feed = FeedCache.getFeed(url);
|
|
|
|
if (feed.downloadCallback)
|
|
feed.downloadCallback.onProgress(feed, aEvent.position, aEvent.totalSize);
|
|
},
|
|
|
|
onDownloadError: function(aEvent)
|
|
{
|
|
var request = aEvent.target;
|
|
var url = request.channel.originalURI.spec;
|
|
var feed = FeedCache.getFeed(url);
|
|
if (feed.downloadCallback)
|
|
{
|
|
// if the http status code is a 304, then the feed has not been modified since we last downloaded it.
|
|
var error = kNewsBlogRequestFailure;
|
|
try
|
|
{
|
|
if (request.status == 304)
|
|
error = kNewsBlogNoNewItems;
|
|
} catch (ex) {}
|
|
feed.downloadCallback.downloaded(feed, error);
|
|
}
|
|
|
|
FeedCache.removeFeed(url);
|
|
},
|
|
|
|
onParseError: function(aFeed)
|
|
{
|
|
if (aFeed && aFeed.downloadCallback)
|
|
{
|
|
if (aFeed.downloadCallback)
|
|
aFeed.downloadCallback.downloaded(aFeed, aFeed.request && aFeed.request.status == 304 ? kNewsBlogNoNewItems : kNewsBlogInvalidFeed);
|
|
FeedCache.removeFeed(aFeed.url);
|
|
}
|
|
},
|
|
|
|
get url()
|
|
{
|
|
var ds = getSubscriptionsDS(this.server);
|
|
var url = ds.GetTarget(this.resource, DC_IDENTIFIER, true);
|
|
if (url)
|
|
url = url.QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
|
|
else
|
|
url = this.resource.Value;
|
|
return url;
|
|
},
|
|
|
|
get title()
|
|
{
|
|
var ds = getSubscriptionsDS(this.server);
|
|
var title = ds.GetTarget(this.resource, DC_TITLE, true);
|
|
if (title)
|
|
title = title.QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
|
|
return title;
|
|
},
|
|
|
|
set title (aNewTitle)
|
|
{
|
|
if (!aNewTitle)
|
|
return;
|
|
|
|
var ds = getSubscriptionsDS(this.server);
|
|
aNewTitle = rdf.GetLiteral(aNewTitle);
|
|
var old_title = ds.GetTarget(this.resource, DC_TITLE, true);
|
|
if (old_title)
|
|
ds.Change(this.resource, DC_TITLE, old_title, aNewTitle);
|
|
else
|
|
ds.Assert(this.resource, DC_TITLE, aNewTitle, true);
|
|
},
|
|
|
|
get lastModified()
|
|
{
|
|
var ds = getSubscriptionsDS(this.server);
|
|
var lastModified = ds.GetTarget(this.resource, DC_LASTMODIFIED, true);
|
|
if (lastModified)
|
|
lastModified = lastModified.QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
|
|
return lastModified;
|
|
},
|
|
|
|
set lastModified(aLastModified)
|
|
{
|
|
var ds = getSubscriptionsDS(this.server);
|
|
aLastModified = rdf.GetLiteral(aLastModified);
|
|
var old_lastmodified = ds.GetTarget(this.resource, DC_LASTMODIFIED, true);
|
|
if (old_lastmodified)
|
|
ds.Change(this.resource, DC_LASTMODIFIED, old_lastmodified, aLastModified);
|
|
else
|
|
ds.Assert(this.resource, DC_LASTMODIFIED, aLastModified, true);
|
|
|
|
// do we need to flush every time this property changes?
|
|
ds = ds.QueryInterface(Components.interfaces.nsIRDFRemoteDataSource);
|
|
ds.Flush();
|
|
},
|
|
|
|
get quickMode ()
|
|
{
|
|
var ds = getSubscriptionsDS(this.server);
|
|
var quickMode = ds.GetTarget(this.resource, FZ_QUICKMODE, true);
|
|
if (quickMode)
|
|
{
|
|
quickMode = quickMode.QueryInterface(Components.interfaces.nsIRDFLiteral);
|
|
quickMode = quickMode.Value;
|
|
quickMode = eval(quickMode);
|
|
}
|
|
return quickMode;
|
|
},
|
|
|
|
set quickMode (aNewQuickMode)
|
|
{
|
|
var ds = getSubscriptionsDS(this.server);
|
|
aNewQuickMode = rdf.GetLiteral(aNewQuickMode);
|
|
var old_quickMode = ds.GetTarget(this.resource, FZ_QUICKMODE, true);
|
|
if (old_quickMode)
|
|
ds.Change(this.resource, FZ_QUICKMODE, old_quickMode, aNewQuickMode);
|
|
else
|
|
ds.Assert(this.resource, FZ_QUICKMODE, aNewQuickMode, true);
|
|
},
|
|
|
|
get link ()
|
|
{
|
|
var ds = getSubscriptionsDS(this.server);
|
|
var link = ds.GetTarget(this.resource, RSS_LINK, true);
|
|
if(link)
|
|
link = link.QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
|
|
return link;
|
|
},
|
|
|
|
set link (aNewLink)
|
|
{
|
|
if (!aNewLink)
|
|
return;
|
|
|
|
var ds = getSubscriptionsDS(this.server);
|
|
aNewLink = rdf.GetLiteral(aNewLink);
|
|
var old_link = ds.GetTarget(this.resource, RSS_LINK, true);
|
|
if (old_link)
|
|
ds.Change(this.resource, RSS_LINK, old_link, aNewLink);
|
|
else
|
|
ds.Assert(this.resource, RSS_LINK, aNewLink, true);
|
|
},
|
|
|
|
parse: function()
|
|
{
|
|
// Figures out what description language (RSS, Atom) and version this feed
|
|
// is using and calls a language/version-specific feed parser.
|
|
|
|
debug("parsing feed " + this.url);
|
|
|
|
if (!this.request.responseText)
|
|
{
|
|
this.onParseError(this);
|
|
return;
|
|
}
|
|
|
|
// create a feed parser which will parse the feed for us
|
|
var parser = new FeedParser();
|
|
this.itemsToStore = parser.parseFeed(this, this.request.responseText, this.request.responseXML, this.request.channel.URI);
|
|
|
|
// storeNextItem will iterate through the parsed items, storing each one.
|
|
this.itemsToStoreIndex = 0;
|
|
this.storeNextItem();
|
|
},
|
|
|
|
invalidateItems: function ()
|
|
{
|
|
var ds = getItemsDS(this.server);
|
|
debug("invalidating items for " + this.url);
|
|
var items = ds.GetSources(FZ_FEED, this.resource, true);
|
|
var item;
|
|
|
|
while (items.hasMoreElements())
|
|
{
|
|
item = items.getNext();
|
|
item = item.QueryInterface(Components.interfaces.nsIRDFResource);
|
|
debug("invalidating " + item.Value);
|
|
var valid = ds.GetTarget(item, FZ_VALID, true);
|
|
if (valid)
|
|
ds.Unassert(item, FZ_VALID, valid, true);
|
|
}
|
|
},
|
|
|
|
removeInvalidItems: function()
|
|
{
|
|
var ds = getItemsDS(this.server);
|
|
debug("removing invalid items for " + this.url);
|
|
var items = ds.GetSources(FZ_FEED, this.resource, true);
|
|
var item;
|
|
while (items.hasMoreElements())
|
|
{
|
|
item = items.getNext();
|
|
item = item.QueryInterface(Components.interfaces.nsIRDFResource);
|
|
if (ds.HasAssertion(item, FZ_VALID, RDF_LITERAL_TRUE, true))
|
|
continue;
|
|
debug("removing " + item.Value);
|
|
ds.Unassert(item, FZ_FEED, this.resource, true);
|
|
if (ds.hasArcOut(item, FZ_FEED))
|
|
debug(item.Value + " is from more than one feed; only the reference to this feed removed");
|
|
else
|
|
removeAssertions(ds, item);
|
|
}
|
|
},
|
|
|
|
createFolder: function()
|
|
{
|
|
if (!this.folder)
|
|
this.server.rootMsgFolder.createSubfolder(this.name, null /* supposed to be a msg window */);
|
|
},
|
|
|
|
// 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.
|
|
// otherwise it triggers a download done notification to the UI
|
|
storeNextItem: function()
|
|
{
|
|
if (!this.itemsToStore || !this.itemsToStore.length)
|
|
{
|
|
this.createFolder();
|
|
this.cleanupParsingState(this);
|
|
return;
|
|
}
|
|
|
|
var item = this.itemsToStore[this.itemsToStoreIndex];
|
|
|
|
item.store();
|
|
item.markValid();
|
|
|
|
// if the listener is tracking progress for storing each item, report it here...
|
|
if (item.feed.downloadCallback && item.feed.downloadCallback.onFeedItemStored)
|
|
item.feed.downloadCallback.onFeedItemStored(item.feed, this.itemsToStoreIndex, this.itemsToStore.length);
|
|
|
|
this.itemsToStoreIndex++
|
|
|
|
// eventually we'll report individual progress here....
|
|
|
|
if (this.itemsToStoreIndex < this.itemsToStore.length)
|
|
{
|
|
if (!this.storeItemsTimer)
|
|
this.storeItemsTimer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
|
|
this.storeItemsTimer.initWithCallback(this, 50, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
|
|
}
|
|
else
|
|
{
|
|
// we have just finished downloading one or more feed items into the destination folder,
|
|
// if the folder is still listed as having new messages in it, then we should set the biff state on the folder
|
|
// so the right RDF UI changes happen in the folder pane to indicate new mail.
|
|
|
|
if (item.feed.folder.hasNewMessages)
|
|
item.feed.folder.biffState = Components.interfaces.nsIMsgFolder.nsMsgBiffState_NewMail;
|
|
this.cleanupParsingState(item.feed);
|
|
}
|
|
},
|
|
|
|
cleanupParsingState: function(aFeed)
|
|
{
|
|
// now that we are done parsing the feed, remove the feed from our feed cache
|
|
FeedCache.removeFeed(aFeed.url);
|
|
aFeed.removeInvalidItems();
|
|
|
|
// let's be sure to flush any feed item changes back to disk
|
|
var ds = getItemsDS(aFeed.server);
|
|
ds.QueryInterface(Components.interfaces.nsIRDFRemoteDataSource).Flush(); // flush any changes
|
|
|
|
if (aFeed.downloadCallback)
|
|
aFeed.downloadCallback.downloaded(aFeed, kNewsBlogSuccess);
|
|
|
|
this.request = null; // force the xml http request to go away. This helps reduce some nasty assertions on shut down.
|
|
this.itemsToStore = "";
|
|
this.itemsToStoreIndex = 0;
|
|
this.storeItemsTimer = null;
|
|
},
|
|
|
|
notify: function(aTimer)
|
|
{
|
|
this.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;
|
|
}
|
|
};
|
|
|