initial checkin of native thunderbird rss extension, NPOB, r/sr=sspitzer

This commit is contained in:
bienvenu%nventure.com 2004-06-16 17:00:27 +00:00
Родитель 657ad8705e
Коммит 275d570d89
20 изменённых файлов: 3741 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,60 @@
#
# ***** 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 Offline Startup Manager.
#
# The Initial Developer of the Original Code is
# David Bienvenu
# Portions created by the Initial Developer are Copyright (C) 2004
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# David Bienvenu, <bienvenu@nventure.com>, original author
#
# Alternatively, the contents of this file may be used under the terms of
# either of 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 *****
DEPTH = ../../..
topsrcdir = @top_srcdir@
srcdir = @srcdir@
VPATH = @srcdir@
include $(DEPTH)/config/autoconf.mk
MODULE = newsblog
include $(DEPTH)/config/autoconf.mk
DIRS += skin
EXTRA_COMPONENTS = js/newsblog.js
include $(topsrcdir)/config/rules.mk
#libs::
# @$(REGCHROME) content messenger-newsblog messenger.jar
#install::
# @$(REGCHROME_INSTALL) content messenger-newsblog messenger.jar

Просмотреть файл

@ -0,0 +1,321 @@
var rdfcontainer =
Components
.classes["@mozilla.org/rdf/container-utils;1"]
.getService(Components.interfaces.nsIRDFContainerUtils);
var rdfparser =
Components
.classes["@mozilla.org/rdf/xml-parser;1"]
.createInstance(Components.interfaces.nsIRDFXMLParser);
// For use when serializing content in Atom feeds.
var serializer = new XMLSerializer;
// 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();
function Feed(url, quickMode, title) {
this.url = url;
this.quickMode = quickMode || false;
this.title = title || null;
this.description = null;
this.author = null;
this.request = null;
return this;
}
// The name of the message folder corresponding to the feed.
// XXX This should be called something more descriptive like "folderName".
// XXX Or maybe, when we support nested folders and downloading into any folder,
// there could just be a reference to the folder itself called "folder".
Feed.prototype.name getter = function() {
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;
}
Feed.prototype.download = function(async, parseItems) {
// Whether or not to download the feed asynchronously.
async = async == null ? true : async ? true : false;
// 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;
this.request = new XMLHttpRequest();
this.request.open("GET", this.url, async);
this.request.overrideMimeType("text/xml");
if (async) {
this.request.onload = Feed.onDownloaded;
this.request.onerror = Feed.onDownloadError;
gFzFeedCache[this.url] = this;
}
this.request.send(null);
if (!async) {
this.parse();
}
}
Feed.onDownloaded = function(event) {
var request = event.target;
var url = request.channel.originalURI.spec;
debug(url + " downloaded");
var feed = gFzFeedCache[url];
if (!feed)
throw("error after downloading " + url + ": couldn't retrieve feed from request");
feed.parse();
}
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");
throw("error downloading feed " + url);
}
Feed.prototype.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) {
throw("error parsing feed " + this.url + ": no data");
return;
}
else if (this.request.responseText.search(/="http:\/\/purl\.org\/rss\/1\.0\/"/) != -1) {
debug(this.url + " is an RSS 1.x (RDF-based) feed");
this.parseAsRSS1();
}
else if (this.request.responseText.search(/="http:\/\/purl.org\/atom\/ns#"/) != -1) {
debug(this.url + " is an Atom feed");
this.parseAsAtom();
}
else if (this.request.responseText.search(/"http:\/\/my\.netscape\.com\/rdf\/simple\/0\.9\/"/) != -1)
{
// RSS 0.9x is forward compatible with RSS 2.0, so use the RSS2 parser to handle it.
debug(this.url + " is an 0.9x feed");
this.parseAsRSS2();
}
// XXX Explicitly check for RSS 2.0 instead of letting it be handled by the
// default behavior (who knows, we may change the default at some point).
else {
// We don't know what kind of feed this is; let's pretend it's RSS 0.9x
// and hope things work out for the best. In theory even RSS 1.0 feeds
// could be parsed by the 0.9x parser if the RSS namespace was the default.
debug(this.url + " is of unknown format; assuming an RSS 0.9x feed");
this.parseAsRSS2();
}
}
Feed.prototype.parseAsRSS2 = function() {
if (!this.request.responseXML || !(this.request.responseXML instanceof XMLDocument))
throw("error parsing RSS 2.0 feed " + this.url + ": data not parsed into XMLDocument object");
// 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");
this.title = this.title || getNodeValue(channel.getElementsByTagName("title")[0]);
this.description = getNodeValue(channel.getElementsByTagName("description")[0]);
if (!this.parseItems)
return;
var itemNodes = this.request.responseXML.getElementsByTagName("item");
for ( var i=0 ; i<itemNodes.length ; i++ ) {
var itemNode = itemNodes[i];
var item = new FeedItem();
item.feed = this;
var link = getNodeValue(itemNode.getElementsByTagName("link")[0]);
var guidNode = itemNode.getElementsByTagName("guid")[0];
if (guidNode) {
var guid = getNodeValue(guidNode);
var isPermaLink =
guidNode.getAttribute('isPermaLink') == 'false' ? false : true;
}
item.url = (guid && isPermaLink) ? guid : link ? link : null;
item.id = guid;
item.description = getNodeValue(itemNode.getElementsByTagName("description")[0]);
item.title = getNodeValue(itemNode.getElementsByTagName("title")[0])
|| (item.description ? item.description.substr(0, 150) : null)
|| item.title;
item.author = getNodeValue(itemNode.getElementsByTagName("author")[0]
|| itemNode.getElementsByTagName("creator")[0]
|| channel.getElementsByTagName("creator")[0])
|| this.title
|| item.author;
item.date = getNodeValue(itemNode.getElementsByTagName("pubDate")[0]
|| itemNode.getElementsByTagName("date")[0])
|| item.date;
item.store();
}
}
Feed.prototype.parseAsRSS1 = function() {
// RSS 1.0 is valid RDF, so use the RDF parser/service to extract data.
// Create a new RDF data source and parse the feed into it.
var ds = Components
.classes["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"]
.createInstance(Components.interfaces.nsIRDFDataSource);
rdfparser.parseString(ds, this.request.channel.URI, this.request.responseText);
// Get information about the feed as a whole.
var channel = ds.GetSource(RDF_TYPE, RSS_CHANNEL, true);
this.title = this.title || getRDFTargetValue(ds, channel, RSS_TITLE);
this.description = getRDFTargetValue(ds, channel, RSS_DESCRIPTION);
if (!this.parseItems)
return;
var items = ds.GetTarget(channel, RSS_ITEMS, true);
//items = items.QueryInterface(Components.interfaces.nsIRDFContainer);
items = rdfcontainer.MakeSeq(ds, items);
items = items.GetElements();
// If the channel doesn't list any items, look for resources of type "item"
// (a hacky workaround for some buggy feeds).
if (!items.hasMoreElements())
items = ds.GetSources(RDF_TYPE, RSS_ITEM, true);
while (items.hasMoreElements()) {
var itemResource = items.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
var item = new FeedItem();
item.feed = this;
// Prefer the value of the link tag to the item URI since the URI could be
// a relative URN.
var uri = itemResource.Value;
var link = getRDFTargetValue(ds, itemResource, RSS_LINK);
item.url = link || uri;
item.id = item.url;
item.description = getRDFTargetValue(ds, itemResource, RSS_DESCRIPTION);
item.title = getRDFTargetValue(ds, itemResource, RSS_TITLE)
|| getRDFTargetValue(ds, itemResource, DC_SUBJECT)
|| (item.description ? item.description.substr(0, 150) : null)
|| item.title;
item.author = getRDFTargetValue(ds, itemResource, DC_CREATOR)
|| getRDFTargetValue(ds, channel, DC_CREATOR)
|| this.title
|| item.author;
item.date = getRDFTargetValue(ds, itemResource, DC_DATE) || item.date;
item.content = getRDFTargetValue(ds, itemResource, RSS_CONTENT_ENCODED);
item.store();
}
}
Feed.prototype.parseAsAtom = function() {
if (!this.request.responseXML || !(this.request.responseXML instanceof XMLDocument))
throw("error parsing Atom feed " + this.url + ": data not parsed into XMLDocument object");
// 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);
this.title = this.title || getNodeValue(channel.getElementsByTagName("title")[0]);
this.description = getNodeValue(channel.getElementsByTagName("tagline")[0]);
if (!this.parseItems)
return;
var items = this.request.responseXML.getElementsByTagName("entry");
for ( var i=0 ; i<items.length ; i++ ) {
var itemNode = items[i];
var item = new FeedItem();
item.feed = this;
var url;
var links = itemNode.getElementsByTagName("link");
for ( var j=0 ; j<links.length ; j++ ) {
var alink = links[j];
if (alink && alink.getAttribute('rel') && alink.getAttribute('rel') == 'alternate' && alink.getAttribute('href')) {
url = alink.getAttribute('href');
break;
}
}
item.url = url;
item.id = getNodeValue(itemNode.getElementsByTagName("id")[0]);
item.description = getNodeValue(itemNode.getElementsByTagName("summary")[0]);
item.title = getNodeValue(itemNode.getElementsByTagName("title")[0])
|| (item.description ? item.description.substr(0, 150) : null)
|| item.title;
var author = itemNode.getElementsByTagName("author")[0]
|| itemNode.getElementsByTagName("contributor")[0]
|| channel.getElementsByTagName("author")[0];
if (author) {
var name = getNodeValue(author.getElementsByTagName("name")[0]);
var email = getNodeValue(author.getElementsByTagName("email")[0]);
if (name)
author = name + (email ? " <" + email + ">" : "");
else if (email)
author = email;
}
item.author = author || item.author || this.title;
item.date = getNodeValue(itemNode.getElementsByTagName("modified")[0]
|| itemNode.getElementsByTagName("issued")[0]
|| itemNode.getElementsByTagName("created")[0])
|| item.date;
// XXX We should get the xml:base attribute from the content tag as well
// and use it as the base HREF of the message.
// XXX Atom feeds can have multiple content elements; we should differentiate
// between them and pick the best one.
// Some Atom feeds wrap the content in a CTYPE declaration; others use
// a namespace to identify the tags as HTML; and a few are buggy and put
// HTML tags in without declaring their namespace so they look like Atom.
// We deal with the first two but not the third.
var content;
var contentNode = itemNode.getElementsByTagName("content")[0];
if (contentNode) {
content = "";
for ( var j=0 ; j<contentNode.childNodes.length ; j++ ) {
var node = contentNode.childNodes.item(j);
if (node.nodeType == node.CDATA_SECTION_NODE)
content += node.data;
else
content += serializer.serializeToString(node);
//content += getNodeValue(node);
}
if (contentNode.getAttribute('mode') == "escaped") {
content = content.replace(/&lt;/g, "<");
content = content.replace(/&gt;/g, ">");
content = content.replace(/&amp;/g, "&");
}
if (content == "")
content = null;
}
item.content = content;
item.store();
}
}

Просмотреть файл

@ -0,0 +1,450 @@
// Hash of items being downloaded, indexed by URL, so the load event listener
// can access the FeedItem objects after it downloads their content.
// XXX Not currently being used, since we're not downloading content these days.
var gFzItemCache = new Object();
// Handy conversion values.
const HOURS_TO_MINUTES = 60;
const MINUTES_TO_SECONDS = 60;
const SECONDS_TO_MILLISECONDS = 1000;
const MINUTES_TO_MILLISECONDS = MINUTES_TO_SECONDS * SECONDS_TO_MILLISECONDS;
const HOURS_TO_MILLISECONDS = HOURS_TO_MINUTES * MINUTES_TO_MILLISECONDS;
function FeedItem() {
// XXX Convert date to a consistent representation in a setter.
this.date = new Date().toString();
return this;
}
FeedItem.prototype.id = null;
FeedItem.prototype.feed = null;
FeedItem.prototype.description = null;
FeedItem.prototype.content = null;
FeedItem.prototype.title = "(no subject)";
FeedItem.prototype.author = "anonymous";
FeedItem.prototype._url = null;
FeedItem.prototype.url getter = function() { return this._url }
FeedItem.prototype.url setter = function(url) {
var uri =
Components
.classes["@mozilla.org/network/standard-url;1"]
.getService(Components.interfaces["nsIStandardURL"]);
uri.init(1, 80, url, null, null);
var uri = uri.QueryInterface(Components.interfaces.nsIURI);
this._url = uri.spec;
}
// A string that identifies the item; currently only used in debug statements.
FeedItem.prototype.identity getter = function() { return this.feed.name + ": " + this.title + " (" + this.id + ")" }
FeedItem.prototype.messageID getter = function() {
// XXX Make this conform to the message ID spec.
var messageID = this.id || this.url || this.title;
// Escape occurrences of message ID meta characters <, >, and @.
messageID.replace(/</g, "%3C");
messageID.replace(/>/g, "%3E");
messageID.replace(/@/g, "%40");
messageID = messageID + "@" + "localhost.localdomain";
return messageID;
}
const MESSAGE_TEMPLATE = "\n\
<html>\n\
<head>\n\
<title>%TITLE%</title>\n\
<style type=\"text/css\">\n\
body {\n\
margin: 0;\n\
border: none;\n\
padding: 0;\n\
}\n\
#toolbar {\n\
position: fixed;\n\
top: 0;\n\
right: 0;\n\
left: 0;\n\
height: 1.4em;\n\
margin: 0;\n\
border-bottom: thin solid black;\n\
padding-left: 0.5em;\n\
background-color: -moz-dialog;\n\
}\n\
%STYLE%\n\
</style>\n\
</head>\n\
<body>\n\
<div id=\"toolbar\">\n\
<a href=\"%URL%\">view in browser</a>\n\
</div>\n\
%CONTENT_TEMPLATE%\n\
</body>\n\
</html>\n\
";
const REMOTE_CONTENT_TEMPLATE = "\n\
<iframe src=\"%URL%\">\n\
%DESCRIPTION%\n\
</iframe>\n\
";
const REMOTE_STYLE = "\n\
iframe {\n\
position: fixed;\n\
top: 1.4em;\n\
right: 0;\n\
bottom: 0;\n\
left: 0;\n\
border: none;\n\
}\n\
";
// Unlike remote content, which is locked within a fixed position iframe,
// local content goes is positioned according to the normal rules of flow.
// The problem with this is that the message pane itself provides a scrollbar
// if necessary, and that scrollbar appears next to the toolbar as well as
// the content being scrolled. The solution is to lock local content within
// a fixed position div and set its overflow property to auto so that the div
// itself provides the scrollbar. Unfortunately we can't do that because of
// Mozilla bug 97283, which makes it hard to scroll an auto overflow div.
const LOCAL_CONTENT_TEMPLATE = "\n\
<div id=\"content\">\n\
%CONTENT%\n\
</div>\n\
";
// We pad the top more to account for the space taken up by the toolbar.
// In theory the top should be padded 1.8em if we want 0.4em of padding between
// the 1.4em high toolbar and the content, but extra padding seems to get added
// to the top of the message, so we have to reduce the top padding accordingly.
const LOCAL_STYLE = "\n\
#content {\n\
margin: 0;\n\
border: none;\n\
padding: 0.4em;\n\
padding-top: 1.8em;\n\
}\n\
";
FeedItem.prototype.store = function() {
if (this.isStored()) {
debug(this.identity + " already stored; ignoring");
}
else if (this.content) {
debug(this.identity + " has content; storing");
var content = MESSAGE_TEMPLATE;
content = content.replace(/%CONTENT_TEMPLATE%/, LOCAL_CONTENT_TEMPLATE);
content = content.replace(/%STYLE%/, LOCAL_STYLE);
content = content.replace(/%TITLE%/, this.title);
content = content.replace(/%URL%/g, this.url);
content = content.replace(/%CONTENT%/, this.content);
this.content = content; // XXX store it elsewhere, f.e. this.page
this.writeToFolder();
}
else if (this.feed.quickMode) {
debug(this.identity + " in quick mode; storing");
this.content = this.description || this.title;
var content = MESSAGE_TEMPLATE;
content = content.replace(/%CONTENT_TEMPLATE%/, LOCAL_CONTENT_TEMPLATE);
content = content.replace(/%STYLE%/, LOCAL_STYLE);
content = content.replace(/%TITLE%/, this.title);
content = content.replace(/%URL%/g, this.url);
content = content.replace(/%CONTENT%/, this.content);
this.content = content; // XXX store it elsewhere, f.e. this.page
this.writeToFolder();
} else {
//debug(this.identity + " needs content; downloading");
debug(this.identity + " needs content; creating and storing");
var content = MESSAGE_TEMPLATE;
content = content.replace(/%CONTENT_TEMPLATE%/, REMOTE_CONTENT_TEMPLATE);
content = content.replace(/%STYLE%/, REMOTE_STYLE);
content = content.replace(/%TITLE%/, this.title);
content = content.replace(/%URL%/g, this.url);
content = content.replace(/%DESCRIPTION%/, this.description || this.title);
this.content = content; // XXX store it elsewhere, f.e. this.page
this.writeToFolder();
//this.download();
}
}
FeedItem.prototype.isStored = function() {
// Checks to see if the item has already been stored in its feed's message folder.
debug(this.identity + " checking to see if stored");
var server = getIncomingServer();
var folder;
try {
//var folder = server.rootMsgFolder.FindSubFolder(feed);
folder = server.rootMsgFolder.getChildNamed(this.feed.name);
} catch(e) {
folder = null;
}
if (!folder) {
debug(this.feed.name + " folder doesn't exist; creating");
server.rootMsgFolder.createSubfolder(this.feed.name, getMessageWindow());
debug(this.identity + " not stored (folder didn't exist)");
return false;
}
try {
folder = folder.QueryInterface(Components.interfaces.nsIMsgFolder);
var db = folder.getMsgDatabase(getMessageWindow());
var hdr = db.getMsgHdrForMessageID(this.messageID);
if (hdr) {
debug(this.identity + " stored");
return true;
}
else {
debug(this.identity + " not stored? let's check all headers");
var foo = db.EnumerateMessages();
var i=0;
while (foo.hasMoreElements()) {
++i;
var bar = foo.getNext();
bar = bar.QueryInterface(Components.interfaces.nsIMsgDBHdr);
if (this.messageID == bar.messageId) {
debug(this.identity + " stored (found it while checking all headers)");
return true;
}
}
debug(this.identity + " not stored (checked " + i + " headers but couldn't find it)");
return false;
}
}
catch(e) {
debug(this.identity + " error checking if stored: " + e);
throw(e);
}
}
FeedItem.prototype.download = function() {
this.request = new XMLHttpRequest();
this.request.item = this;
this.request.open("GET", this.url);
this.request.onload = FeedItem.onDownloaded;
this.request.onerror = FeedItem.onDownloadError;
this.request.send(null);
//updateServerBusyState(1);
gFzItemCache[this.url] = this;
}
FeedItem.onDownloaded = function(event) {
var request = event.target;
var url = request.channel.originalURI.spec;
//updateServerBusyState(-1);
var item = gFzItemCache[url];
if (!item)
throw("error after downloading " + url + ": couldn't retrieve item from request");
if (!request.responseText)
throw(item.identity + " content supposedly downloaded but missing");
debug(item.identity + ": content downloaded");
item.content = request.responseText;
item.writeToFolder();
delete gFzItemCache[url];
}
FeedItem.onDownloadError = function(event) {
// XXX add error message if available and notify the user?
var request = event.target;
var url = request.channel.originalURI.spec;
var item = gFzItemCache[url];
throw("error downloading item " + (item ? item.identity : url));
}
FeedItem.unicodeConverter =
Components
.classes["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
FeedItem.unicodeConverter.charset = "UTF-8";
FeedItem.prototype.toUtf8 = function(str) {
return FeedItem.unicodeConverter.ConvertFromUnicode(str);
}
FeedItem.prototype.writeToFolder = function() {
debug(this.identity + " writing to message folder");
var server = getIncomingServer();
// XXX Should we really be modifying the original data here instead of making
// a copy of it? Currently we never use the item object again after writing it
// to the message folder, but will that always be the case?
// If the sender isn't a valid email address, quote it so it looks nicer.
if (this.author && this.author.indexOf('@') == -1)
this.author = '<' + this.author + '>';
// Compress white space in the subject to make it look better.
this.title = this.title.replace(/[\t\r\n]+/g, " ");
// If the date looks like it's in W3C-DTF format, convert it into
// an IETF standard date. Otherwise assume it's in IETF format.
if (this.date.search(/^\d\d\d\d/) != -1)
this.date = W3CToIETFDate(this.date);
// Escape occurrences of "From " at the beginning of lines of content
// per the mbox standard, since "From " denotes a new message, and add
// a line break so we know the last line has one.
this.content = this.content.replace(/([\r\n]+)(>*From )/g, "$1>$2");
this.content += "\n";
// The opening line of the message, mandated by standards to start with
// "From ". It's useful to construct this separately because we not only
// need to write it into the message, we also need to use it to calculate
// the offset of the X-Mozilla-Status lines from the front of the message
// for the statusOffset property of the DB header object.
var openingLine = 'From - ' + this.date + '\n';
var source =
openingLine +
'X-Mozilla-Status: 0000\n' +
'X-Mozilla-Status2: 00000000\n' +
'Date: ' + this.date + '\n' +
'Message-Id: <' + this.messageID + '>\n' +
'From: ' + this.author + '\n' +
'MIME-Version: 1.0\n' +
'Subject: ' + this.title + '\n' +
'Content-Type: text/html; charset=UTF-8\n' +
'Content-Transfer-Encoding: 8bit\n' +
'Content-Base: ' + this.url + '\n' +
'\n' +
this.content;
debug(this.identity + " is " + source.length + " characters long");
// Get the folder and database storing the feed's messages and headers.
var folder = server.rootMsgFolder.getChildNamed(this.feed.name);
folder = folder.QueryInterface(Components.interfaces.nsIMsgFolder);
var mbox = new LocalFile(folder.path.nativePath,
MODE_WRONLY | MODE_APPEND | MODE_CREATE);
// Create a message header object using the offset of the message
// from the front of the mbox file as the unique message key (which seems
// to be what other mail code does).
var key = mbox.localFile.fileSize;
var length = mbox.write(this.toUtf8(source));
mbox.flush();
mbox.close();
var db = folder.getMsgDatabase(getMessageWindow());
var header = db.CreateNewHdr(key);
// Not sure what date should be, but guessing it's seconds-since-epoch
// since that's what existing values look like. For some reason the values
// in the database have three more decimal places than the values produced
// by Date.getTime(), and in the DB values I've inspected they were all zero,
// so I append three zeros here, which seems to work. Sheesh what a hack.
// XXX Figure out what's up with that, maybe milliseconds?
header.date = new Date(this.date).getTime() + "000";
header.Charset = "UTF-8";
header.author = this.toUtf8(this.author);
header.subject = this.toUtf8(this.title);
header.messageId = this.messageID;
header.messageSize = length;
// Count the number of line break characters to determine the line count.
header.lineCount = this.content.match(/\r?\n?/g).length;
header.messageOffset = key;
header.statusOffset = openingLine.length;
db.AddNewHdrToDB(header, true);
}
function W3CToIETFDate(dateString) {
// Converts a W3C-DTF (subset of ISO 8601) date string to an IETF date string.
// W3C-DTF is described in this note: http://www.w3.org/TR/NOTE-datetime
// IETF is obtained via the Date object's toUTCString() method. The object's
// toString() method is insufficient because it spells out timezones on Win32
// (f.e. "Pacific Standard Time" instead of "PST"), which Mail doesn't grok.
// For info, see http://lxr.mozilla.org/mozilla/source/js/src/jsdate.c#1526.
var parts = dateString.match(/(\d\d\d\d)(-(\d\d))?(-(\d\d))?(T(\d\d):(\d\d)(:(\d\d)(\.(\d+))?)?(Z|([+-])(\d\d):(\d\d))?)?/);
debug("date parts: " + parts);
// Here's an example of a W3C-DTF date string and what .match returns for it.
// date: 2003-05-30T11:18:50.345-08:00
// date.match returns array values:
// 0: 2003-05-30T11:18:50-08:00,
// 1: 2003,
// 2: -05,
// 3: 05,
// 4: -30,
// 5: 30,
// 6: T11:18:50-08:00,
// 7: 11,
// 8: 18,
// 9: :50,
// 10: 50,
// 11: .345,
// 12: 345,
// 13: -08:00,
// 14: -,
// 15: 08,
// 16: 00
// Create a Date object from the date parts. Note that the Date object
// apparently can't deal with empty string parameters in lieu of numbers,
// so optional values (like hours, minutes, seconds, and milliseconds)
// must be forced to be numbers.
var date = new Date(parts[1], parts[3]-1, parts[5], parts[7] || 0,
parts[8] || 0, parts[10] || 0, parts[12] || 0);
// We now have a value that the Date object thinks is in the local timezone
// but which actually represents the date/time in the remote timezone
// (f.e. the value was "10:00 EST", and we have converted it to "10:00 PST"
// instead of "07:00 PST"). We need to correct that. To do so, we're going
// to add the offset between the remote timezone and UTC (to convert the value
// to UTC), then add the offset between UTC and the local timezone (to convert
// the value to the local timezone).
// Ironically, W3C-DTF gives us the offset between UTC and the remote timezone
// rather than the other way around, while the getTimezoneOffset() method
// of a Date object gives us the offset between the local timezone and UTC
// rather than the other way around. Both of these are the additive inverse
// (i.e. -x for x) of what we want, so we have to invert them to use them
// by multipying by -1
// (f.e. if "the offset between UTC and the remote timezone" is -5 hours,
// then "the offset between the remote timezone and UTC" is -5*-1 = 5 hours).
// Note that if the timezone portion of the date/time string is absent
// (which violates W3C-DTF, although ISO 8601 allows it), we assume the value
// to be in UTC.
// The offset between the remote timezone and UTC in milliseconds.
var remoteToUTCOffset = 0;
if (parts[13] && parts[13] != "Z") {
var direction = (parts[14] == "+" ? 1 : -1);
if (parts[15])
remoteToUTCOffset += direction * parts[15] * HOURS_TO_MILLISECONDS;
if (parts[16])
remoteToUTCOffset += direction * parts[16] * MINUTES_TO_MILLISECONDS;
}
remoteToUTCOffset = remoteToUTCOffset * -1; // invert it
debug("date remote to UTC offset: " + remoteToUTCOffset);
// The offset between UTC and the local timezone in milliseconds.
var UTCToLocalOffset = date.getTimezoneOffset() * MINUTES_TO_MILLISECONDS;
UTCToLocalOffset = UTCToLocalOffset * -1; // invert it
debug("date UTC to local offset: " + UTCToLocalOffset);
date.setTime(date.getTime() + remoteToUTCOffset + UTCToLocalOffset);
debug("date string: " + date.toUTCString());
return date.toUTCString();
}

Просмотреть файл

@ -0,0 +1,35 @@
<?xml version="1.0"?>
<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:chrome="http://www.mozilla.org/rdf/chrome#">
<!-- list all the packages being supplied by this jar -->
<RDF:Seq about="urn:mozilla:package:root">
<RDF:li resource="urn:mozilla:package:messenger-newsblog"/>
</RDF:Seq>
<!-- package information -->
<RDF:Description about="urn:mozilla:package:messenger-newsblog"
chrome:displayName="News and Blog Feed Support"
chrome:author="Team Thunderbird"
chrome:name="messenger-newsblog"
chrome:description="This extension adds the ability to read news and blog feeds to Mozilla Thunderbird."
chrome:extension="true"
>
</RDF:Description>
<!-- overlay information -->
<RDF:Seq about="urn:mozilla:overlays">
<RDF:li resource="chrome://messenger/content/messenger.xul"/>
<RDF:li resource="chrome://messenger/content/mailWindowOverlay.xul"/>
</RDF:Seq>
<RDF:Seq about="chrome://messenger/content/messenger.xul">
<RDF:li>chrome://messenger-newsblog/content/forumzilla.xul</RDF:li>
</RDF:Seq>
<RDF:Seq about="chrome://messenger/content/mailWindowOverlay.xul">
<RDF:li>chrome://messenger-newsblog/content/toolbar-icon.xul</RDF:li>
</RDF:Seq>
</RDF:RDF>

Просмотреть файл

@ -0,0 +1,48 @@
function enumerateInterfaces(obj)
{
var interfaces = new Array();
for (i in Components.interfaces)
{
try
{
obj.QueryInterface(Components.interfaces[i]);
interfaces.push(i);
}
catch(e) {}
}
return interfaces;
}
function enumerateProperties(obj, excludeComplexTypes)
{
var properties = "";
for (p in obj)
{
try
{
if (excludeComplexTypes
&& (typeof obj[p] == 'object' || typeof obj[p] == 'function')) next;
properties += p + " = " + obj[p] + "\n";
}
catch(e) {
properties += p + " = " + e + "\n";
}
}
return properties;
}
// minimal implementation of nsIOutputStream for use by dumpRDF, adapted from
// http://groups.google.com/groups?selm=20011203111618.C1302%40erde.jan.netgaroo.de
var DumpOutputStream = {
write: function(buf, count) { dump(buf); return count; }
};
function dumpRDF( aDS ) {
var serializer = Components.classes["@mozilla.org/rdf/xml-serializer;1"]
.createInstance( Components.interfaces.nsIRDFXMLSerializer );
serializer.init( aDS );
serializer.QueryInterface( Components.interfaces.nsIRDFXMLSource )
.Serialize( DumpOutputStream );
}

Просмотреть файл

@ -0,0 +1,177 @@
<?xml version="1.0"?>
<bindings id="treeEditBindings"
xmlns="http://www.mozilla.org/xbl"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<binding id="edittree" extends="chrome://global/content/bindings/tree.xml#tree">
<content>
<children includes="treecols"/>
<xul:stack flex="1">
<xul:treerows class="tree-rows" flex="1">
<children/>
</xul:treerows>
<xul:textbox ileattr="text" left="0" top="0" hidden="true"/>
</xul:stack>
</content>
<implementation>
<field name="_editOriginalValue">0</field>
<field name="_editRow">-1</field>
<field name="_editCol">null</field>
<field name="onAccept">null</field>
<method name="setEditMode">
<parameter name="x"/>
<parameter name="y"/>
<parameter name="val"/>
<body>
<![CDATA[
var txt = document.getAnonymousElementByAttribute(this, "ileattr", "text");
if (val){
if (x < 0) return;
var originalValue = this.view.getCellText(x,y);
var cellnode = this.getCellNodeAt(x,y);
if (!(cellnode || this.view.isEditable(x,y))) return;
if (this._editRow >= 0) this._assignValueToCell(txt.value,true);
if (cellnode && cellnode.getAttribute("readonly")) return;
txt.removeAttribute("hidden");
var treeBox = this.treeBoxObject;
var outx = {}, outy = {}, outwidth = {}, outheight = {};
var coords = treeBox.getCoordsForCellItem(x,y,"cell",outx,outy,outwidth,outheight);
this._editRow = x;
this._editCol = y;
txt.setAttribute("left",outx.value-3);
txt.setAttribute("top",outy.value-3);
txt.setAttribute("height",outheight.value);
txt.setAttribute("width",outwidth.value - outy.value);
this._editOriginalValue = originalValue;
if (cellnode) cellnode.setAttribute("label","");
this.view.setCellText(x,y,"");
txt.value = originalValue;
txt.select();
this.setAttribute("editing","true");
txt.addEventListener("keypress", this.fieldKeyDown, false);
txt.addEventListener("blur", this.fieldChange, true);
}
else {
this.removeAttribute("editing");
txt.setAttribute("hidden","true");
txt.removeEventListener("keypress", this.fieldKeyDown, false);
txt.removeEventListener("blur", this.fieldChange, true);
txt.blur();
}
]]>
</body>
</method>
<method name="getCellNodeAt">
<parameter name="row"/>
<parameter name="col"/>
<body>
var view;
try {
view = this.contentView;
} catch (ex){}
if (view){
var elem = view.getItemAtIndex(row);
if (elem){
var pos = ((document.getElementById(col).ordinal - 1) >> 1);
return elem.firstChild.childNodes[pos];
}
}
return null;
</body>
</method>
<method name="fieldKeyDown">
<parameter name="aEvent"/>
<body>
<![CDATA[
var tree = aEvent.target;
while (tree && tree.tagName != "tree") tree = tree.parentNode;
if (aEvent.keyCode == 13){
tree._assignValueToCell(this.value,true);
}
if (aEvent.keyCode == 27){
tree._assignValueToCell(tree._editOriginalValue,false);
}
aEvent.preventBubble();
]]>
</body>
</method>
<method name="_assignValueToCell">
<parameter name="value"/>
<parameter name="acceptMode"/>
<body>
<![CDATA[
var rdf = Components
.classes["@mozilla.org/rdf/rdf-service;1"]
.getService(Components.interfaces.nsIRDFService);
if (this._editRow == -1) return;
if (acceptMode && this.onAccept &&
this.onAccept(this._editRow,this._editCol,this._editOriginalValue,value))
return;
var cellnode = this.getCellNodeAt(this._editRow,this._editCol);
if (cellnode) {
cellnode.setAttribute("label", value);
var item = cellnode;
while (item && item.tagName != "treeitem")
item = item.parentNode;
if (this._editCol == "subs-name-column") {
updateTitle(item.id, value);
}
else if (this._editCol == "subs-url-column") {
updateURL(item.id, value);
}
}
this.view.setCellText(this._editRow,this._editCol,value);
this.treeBoxObject.invalidateCell(this._editRow,this._editCol);
this._editRow = -1;
this._editCol = null;
this.builder.rebuild();
this.setEditMode("normal");
]]>
</body>
</method>
<method name="fieldChange">
<parameter name="aEvent"/>
<body>
<![CDATA[
var tree = aEvent.target;
while (tree && tree.tagName != "tree") tree = tree.parentNode;
tree._assignValueToCell(this.value,true);
]]>
</body>
</method>
</implementation>
<handlers>
<handler event="click" clickcount="2" phase="capturing">
<![CDATA[
var treeBox = this.treeBoxObject;
var row = {};
var col = {};
var obj = {};
if(row.value != -1) {
event.preventBubble();
treeBox.getCellAt(event.clientX,event.clientY,row,col,obj);
this.setEditMode(row.value,col.value,true);
}
]]>
</handler>
</handlers>
</binding>
</bindings>

Просмотреть файл

@ -0,0 +1,301 @@
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
*
* 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 JavaScript Debugger
*
* The Initial Developer of the Original Code is
* Netscape Communications Corporation
* Portions created by Netscape are
* Copyright (C) 1998 Netscape Communications Corporation.
*
* Alternatively, the contents of this file may be used under the
* terms of the GNU Public License (the "GPL"), in which case the
* provisions of the GPL are applicable instead of those above.
* If you wish to allow use of your version of this file only
* under the terms of the GPL and not to allow others to use your
* version of this file under the MPL, indicate your decision by
* deleting the provisions above and replace them with the notice
* and other provisions required by the GPL. If you do not delete
* the provisions above, a recipient may use your version of this
* file under either the MPL or the GPL.
*
* Contributor(s):
* Robert Ginda, <rginda@netscape.com>, original author
*
*/
/* notice that these valuse are octal. */
const PERM_IRWXU = 00700; /* read, write, execute/search by owner */
const PERM_IRUSR = 00400; /* read permission, owner */
const PERM_IWUSR = 00200; /* write permission, owner */
const PERM_IXUSR = 00100; /* execute/search permission, owner */
const PERM_IRWXG = 00070; /* read, write, execute/search by group */
const PERM_IRGRP = 00040; /* read permission, group */
const PERM_IWGRP = 00020; /* write permission, group */
const PERM_IXGRP = 00010; /* execute/search permission, group */
const PERM_IRWXO = 00007; /* read, write, execute/search by others */
const PERM_IROTH = 00004; /* read permission, others */
const PERM_IWOTH = 00002; /* write permission, others */
const PERM_IXOTH = 00001; /* execute/search permission, others */
const MODE_RDONLY = 0x01;
const MODE_WRONLY = 0x02;
const MODE_RDWR = 0x04;
const MODE_CREATE = 0x08;
const MODE_APPEND = 0x10;
const MODE_TRUNCATE = 0x20;
const MODE_SYNC = 0x40;
const MODE_EXCL = 0x80;
const PICK_OK = Components.interfaces.nsIFilePicker.returnOK;
const PICK_CANCEL = Components.interfaces.nsIFilePicker.returnCancel;
const PICK_REPLACE = Components.interfaces.nsIFilePicker.returnReplace;
const FILTER_ALL = Components.interfaces.nsIFilePicker.filterAll;
const FILTER_HTML = Components.interfaces.nsIFilePicker.filterHTML;
const FILTER_TEXT = Components.interfaces.nsIFilePicker.filterText;
const FILTER_IMAGES = Components.interfaces.nsIFilePicker.filterImages;
const FILTER_XML = Components.interfaces.nsIFilePicker.filterXML;
const FILTER_XUL = Components.interfaces.nsIFilePicker.filterXUL;
// evald f = fopen("/home/rginda/foo.txt", MODE_WRONLY | MODE_CREATE)
// evald f = fopen("/home/rginda/vnk.txt", MODE_RDONLY)
var futils = new Object();
futils.umask = PERM_IWOTH | PERM_IWGRP;
futils.MSG_SAVE_AS = "Save As";
futils.MSG_OPEN = "Open";
futils.getPicker =
function futils_nosepicker(initialPath, typeList, attribs)
{
const classes = Components.classes;
const interfaces = Components.interfaces;
const PICKER_CTRID = "@mozilla.org/filepicker;1";
const LOCALFILE_CTRID = "@mozilla.org/file/local;1";
const nsIFilePicker = interfaces.nsIFilePicker;
const nsILocalFile = interfaces.nsILocalFile;
var picker = classes[PICKER_CTRID].createInstance(nsIFilePicker);
if (typeof attribs == "object")
{
for (var a in attribs)
picker[a] = attribs[a];
}
else
throw "bad type for param |attribs|";
if (initialPath)
{
var localFile;
if (typeof initialPath == "string")
{
localFile =
classes[LOCALFILE_CTRID].createInstance(nsILocalFile);
localFile.initWithPath(initialPath);
}
else
{
if (!(initialPath instanceof nsILocalFile))
throw "bad type for argument |initialPath|";
localFile = initialPath;
}
picker.displayDirectory = localFile
}
if (typeof typeList == "string")
typeList = typeList.split(" ");
if (typeList instanceof Array)
{
for (var i in typeList)
{
switch (typeList[i])
{
case "$all":
picker.appendFilters(FILTER_ALL);
break;
case "$html":
picker.appendFilters(FILTER_HTML);
break;
case "$text":
picker.appendFilters(FILTER_TEXT);
break;
case "$images":
picker.appendFilters(FILTER_IMAGES);
break;
case "$xml":
picker.appendFilters(FILTER_XML);
break;
case "$xul":
picker.appendFilters(FILTER_XUL);
break;
default:
picker.appendFilter(typeList[i], typeList[i]);
break;
}
}
}
return picker;
}
function pickSaveAs (title, typeList, defaultFile, defaultDir)
{
if (!defaultDir && "lastSaveAsDir" in futils)
defaultDir = futils.lastSaveAsDir;
var picker = futils.getPicker (defaultDir, typeList,
{defaultString: defaultFile});
picker.init (window, title ? title : futils.MSG_SAVE_AS,
Components.interfaces.nsIFilePicker.modeSave);
var rv = picker.show();
if (rv != PICK_CANCEL)
futils.lastSaveAsDir = picker.file.parent;
return {reason: rv, file: picker.file, picker: picker};
}
function pickOpen (title, typeList, defaultFile, defaultDir)
{
if (!defaultDir && "lastOpenDir" in futils)
defaultDir = futils.lastOpenDir;
var picker = futils.getPicker (defaultDir, typeList,
{defaultString: defaultFile});
picker.init (window, title ? title : futils.MSG_OPEN,
Components.interfaces.nsIFilePicker.modeOpen);
var rv = picker.show();
if (rv != PICK_CANCEL)
futils.lastOpenDir = picker.file.parent;
return {reason: rv, file: picker.file, picker: picker};
}
function fopen (path, mode, perms, tmp)
{
return new LocalFile(path, mode, perms, tmp);
}
function LocalFile(file, mode, perms, tmp)
{
const classes = Components.classes;
const interfaces = Components.interfaces;
const LOCALFILE_CTRID = "@mozilla.org/file/local;1";
const FILEIN_CTRID = "@mozilla.org/network/file-input-stream;1";
const FILEOUT_CTRID = "@mozilla.org/network/file-output-stream;1";
const SCRIPTSTREAM_CTRID = "@mozilla.org/scriptableinputstream;1";
const nsIFile = interfaces.nsIFile;
const nsILocalFile = interfaces.nsILocalFile;
const nsIFileOutputStream = interfaces.nsIFileOutputStream;
const nsIFileInputStream = interfaces.nsIFileInputStream;
const nsIScriptableInputStream = interfaces.nsIScriptableInputStream;
if (typeof perms == "undefined")
perms = 0666 & ~futils.umask;
if (typeof file == "string")
{
this.localFile = classes[LOCALFILE_CTRID].createInstance(nsILocalFile);
this.localFile.initWithPath(file);
}
else if (file instanceof nsILocalFile)
{
this.localFile = file;
}
else if (file instanceof Array && file.length > 0)
{
this.localFile = classes[LOCALFILE_CTRID].createInstance(nsILocalFile);
this.localFile.initWithPath(file.shift());
while (file.length > 0)
this.localFile.appendRelativePath(file.shift());
}
else
{
throw "bad type for argument |file|.";
}
if (mode & (MODE_WRONLY | MODE_RDWR))
{
this.outputStream =
classes[FILEOUT_CTRID].createInstance(nsIFileOutputStream);
this.outputStream.init(this.localFile, mode, perms, 0);
}
if (mode & (MODE_RDONLY | MODE_RDWR))
{
var is = classes[FILEIN_CTRID].createInstance(nsIFileInputStream);
is.init(this.localFile, mode, perms, tmp);
this.inputStream =
classes[SCRIPTSTREAM_CTRID].createInstance(nsIScriptableInputStream);
this.inputStream.init(is);
}
}
LocalFile.prototype.write =
function fo_write(buf)
{
if (!("outputStream" in this))
throw "file not open for writing.";
return this.outputStream.write(buf, buf.length);
}
LocalFile.prototype.read =
function fo_read(max)
{
if (!("inputStream" in this))
throw "file not open for reading.";
var av = this.inputStream.available();
if (typeof max == "undefined")
max = av;
if (!av)
return null;
var rv = this.inputStream.read(max);
return rv;
}
LocalFile.prototype.close =
function fo_close()
{
if ("outputStream" in this)
this.outputStream.close();
if ("inputStream" in this)
this.inputStream.close();
}
LocalFile.prototype.flush =
function fo_close()
{
return this.outputStream.flush();
}

Просмотреть файл

@ -0,0 +1,302 @@
// XXX MAKE SURE ENSURANCE_DELAY AND DEBUG ARE CORRECT BEFORE SHIPPING!!!
// How long to wait after accessing all the message databases before we start
// downloading feeds and items. The goal is to wait the minimum length of time
// to ensure the databases are available when we check to see if items are
// already downloaded. This works around the problem in which the databases
// claim not to know about messages that have already been downloaded if we ask
// them too soon after retrieving a reference to them, resulting in duplicate
// messages as we redownload items thinking they are new.
//const ENSURANCE_DELAY = 2000; // XXX FOR TESTING ONLY
const ENSURANCE_DELAY = 15000;
// The name of the local mail server in which Forumzilla creates feed folders.
// XXX Make this configurable.
const SERVER_NAME = "News & Blogs";
// Number of items currently being loaded. gFzIncomingServer.serverBusy will be
// true while this number is greater than zero.
//var gFzMessagesBeingLoaded = 0;
var gFzPrefs =
Components
.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefService)
.getBranch("forumzilla.");
var gFzStartupDelay;
try { gFzStartupDelay = gFzPrefs.getIntPref("startup.delay") }
catch(e) { gFzStartupDelay = 2; }
// Record when we started up so we can give up trying certain things
// repetitively after some time.
var gFzStartupTime = new Date();
// Load and cache the subscriptions data source so it's available when we need it.
getSubscriptionsDS();
function onLoad() {
// XXX Code to make the News & Blogs toolbar button show up automatically.
// Commented out because it isn't working quite right.
//var toolbar = document.getElementById('mail-bar');
//var currentset = toolbar.getAttribute('currentset');
//if (!currentset.search("button-newsandblogs") == -1)
// toolbar.insertItem('button-newsandblogs', 'button-stop', null, true);
// currentset = currentset.replace(/spring/, "button-newsandblogs,spring");
//if (!currentset.search("button-newsandblogs") == -1)
// currentset += ",button-newsandblogs";
//toolbar.setAttribute('currentset', currentset);
// Make sure the subscriptions data source is loaded, since we'll need it
// for everything we do. If it's not loaded, recheck it every second until
// it's been ten seconds since we started up, then give up.
var ds = getSubscriptionsDS();
var remote = ds.QueryInterface(Components.interfaces.nsIRDFRemoteDataSource);
if (!remote.loaded) {
if (new Date() - gFzStartupTime < 10 * 1000)
window.setTimeout(onLoad, 1000);
else
throw("couldn't load the subscriptions datasource within ten seconds");
return;
}
var subs = getSubscriptionsFile();
var oldSubs = getOldSubscriptionsFile();
if (oldSubs.exists())
migrateSubscriptions(oldSubs);
getAccount();
ensureDatabasesAreReady();
// Wait a few seconds to give the message databases time to populate themselves.
// I'm sure there's a better way to determine when the databases are populated
// than this hacky approach, but I don't know what it is.
//downloadFeeds();
window.setTimeout(downloadFeeds, ENSURANCE_DELAY);
}
// Wait a few seconds before starting Forumzilla so we don't grab the UI thread
// before the host application has a chance to display itself. Also, starting
// on load used to crash the host app, although that's not a problem anymore.
//addEventListener("load", onLoad, false);
window.setTimeout(onLoad, gFzStartupDelay * 1000);
function downloadFeeds() {
// Reload subscriptions every 30 minutes (XXX make this configurable via a pref).
// XXX We call onLoad() instead of downloadFeeds() directly because onLoad calls
// ensureDatabasesAreReady() and then delays calling downloadFeeds() for a few
// seconds to make sure the header database is ready before we download feeds
// and messages, which works around our duplicate message problem.
//window.setTimeout(downloadFeeds, 30 * 60 * 1000);
window.setTimeout(onLoad, 30 * 60 * 1000);
var ds = getSubscriptionsDS();
var feeds = getSubscriptionsList().GetElements();
var feed, url, quickMode, title;
while(feeds.hasMoreElements()) {
feed = feeds.getNext();
feed = feed.QueryInterface(Components.interfaces.nsIRDFResource);
url = ds.GetTarget(feed, DC_IDENTIFIER, true);
if (url)
url = url.QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
quickMode = ds.GetTarget(feed, FZ_QUICKMODE, true);
if (quickMode) {
quickMode = quickMode.QueryInterface(Components.interfaces.nsIRDFLiteral);
quickMode = quickMode.Value;
quickMode = eval(quickMode);
}
title = ds.GetTarget(feed, DC_TITLE, true);
if (title)
title = title.QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
feed = new Feed(url, quickMode, title);
feed.download();
}
}
function migrateSubscriptions(oldFile) {
var oldFile2 = new LocalFile(oldFile, MODE_RDONLY | MODE_CREATE);
var subscriptions = oldFile2.read();
if (subscriptions && subscriptions.length > 0) {
subscriptions = subscriptions.split(/[\r\n]+/);
var feeds = getSubscriptionsList();
var url, quickMode;
for ( var i=0 ; i<subscriptions.length ; i++ ) {
url = subscriptions[i];
quickMode = 0;
// Trim whitespace around the URL.
url = url.replace(/^\s+/, "");
url = url.replace(/\s+$/, "");
// If the URL is prefixed by a dollar sign, the feed is in quick mode,
// which means it won't download item content but rather construct a message
// from whatever information is available in the feed file itself
// (i.e. content, description, etc.).
if (url[0] == '$') {
quickMode = 1;
url = url.substr(1);
url = url.replace(/^\s+/, "");
}
// Ignore blank lines and comments.
if (url.length == 0 || url[0] == "#")
continue;
// ### passing in null for the folder may not work...but we may not care
// about migrating old subscriptions
if (feeds.IndexOf(rdf.GetResource(url)) == -1)
addFeed(url, null, quickMode, null);
}
}
oldFile2.close();
oldFile.moveTo(null, "subscriptions.txt.bak");
}
function ensureDatabasesAreReady() {
debug("ensuring databases are ready for feeds");
getIncomingServer();
var folder;
var db;
var folders = gFzIncomingServer.rootMsgFolder.GetSubFolders();
var done = false;
while (!done) {
try {
folder = folders.currentItem();
folder = folder.QueryInterface(Components.interfaces.nsIMsgFolder);
debug("ensuring database is ready for feed " + folder.name);
try {
db = folder.getMsgDatabase(msgWindow);
} catch(e) {
debug("error getting database: " + e);
}
if (!db) {
debug("couldn't get database");
}
else {
debug("got database " + db);
}
folders.next();
}
catch (e) {
done = true;
}
}
debug("ensurance completed; databases should be ready for feeds");
}
function getOldSubscriptionsFile() {
// Get the app directory service so we can look up the user's profile dir.
var appDirectoryService =
Components
.classes["@mozilla.org/file/directory_service;1"]
.getService(Components.interfaces.nsIProperties);
if ( !appDirectoryService )
throw("couldn't retrieve the directory service");
// Get the user's profile directory.
var profileDir =
appDirectoryService.get("ProfD", Components.interfaces.nsIFile);
if ( !profileDir )
throw ("couldn't retrieve the user's profile directory");
// Get the user's subscriptions file.
var subscriptionsFile = profileDir.clone();
subscriptionsFile.append("subscriptions.txt");
return subscriptionsFile;
}
var gFzIncomingServer; // cache
function getIncomingServer() {
if (gFzIncomingServer)
return gFzIncomingServer;
gFzIncomingServer = accountManager.FindServer("nobody", SERVER_NAME, "rss");
return gFzIncomingServer;
}
function getMessageWindow() {
return msgWindow;
}
var gFzAccount; // cache
function getAccount() {
if (gFzAccount)
return gFzAccount;
var accounts = accountManager.accounts;
accounts = accounts.QueryInterface(Components.interfaces.nsICollection);
for ( var i=0 ; i<accounts.Count() ; i++ ) {
var account = accounts.GetElementAt(i);
account = account.QueryInterface(Components.interfaces.nsIMsgAccount);
if (account.incomingServer.prettyName == SERVER_NAME) {
gFzAccount = account;
return gFzAccount;
}
}
// If we made it this far there's no "Feeds" account yet, so let's create one.
debug("no account; creating one...");
gFzAccount = createAccount();
return gFzAccount;
}
function createAccount() {
// I don't think we need an identity, at least not yet. If we did, though,
// this is how we would create it, and then we'd use the commented-out
// addIdentity() call below to add it to the account.
//var identity = accountManager.createIdentity();
//identity.email="<INSERT IDENTITY HERE>";
var server = accountManager.createIncomingServer("nobody", SERVER_NAME, "rss");
// XXX What's the difference between "name" and "prettyName"?
// This seems to set the name, not the pretty name, but it does what I want,
// which is to display this name in the folder pane of the mail window.
server.prettyName = SERVER_NAME;
var account = accountManager.createAccount();
if (!account)
throw("couldn't create account");
account.incomingServer = server;
//account.addIdentity(identity);
return account;
}
/*
function updateServerBusyState(numMessagesLoaded) {
// Not sure if we need this. nsMovemailService.cpp does it, and it might
// fix the problem where the user opens the folder while messages
// are being downloaded and the folder stops updating until the application
// is restarted. Because we load messages asynchronously, we have to
// count the number of messages we're loading so we can set serverBusy
// to false when all messages have been loaded.
gFzMessagesBeingLoaded += numMessagesLoaded;
getIncomingServer();
if (!gFzIncomingServer.serverBusy && gFzMessagesBeingLoaded > 0) {
gFzIncomingServer.serverBusy = true;
debug("marking Feeds server as being busy");
}
else if (gFzIncomingServer.serverBusy && gFzMessagesBeingLoaded <= 0) {
gFzIncomingServer.serverBusy = false;
debug("marking Feeds server as no longer being busy");
}
}
*/

Просмотреть файл

Просмотреть файл

@ -0,0 +1,318 @@
/*
* Global Constants and Variables
*/
// forumzilla.js gets loaded by the main mail window, which provides this,
// but this script runs in its own window, which doesn't, so we need
// to provide it ourselves.
var accountManager =
Components
.classes["@mozilla.org/messenger/account-manager;1"]
.getService();
/*
* Event Handlers
*/
function doLoad() {
// Display the list of feed subscriptions.
var file = getSubscriptionsFile();
var ds = getSubscriptionsDS();
var tree = document.getElementById('subscriptions');
tree.database.AddDataSource(ds);
tree.builder.rebuild();
}
function doAdd() {
var url = window.prompt("Location:", "");
if (url == null)
return;
const DEFAULT_FEED_TITLE = "feed title";
const DEFAULT_FEED_URL = "feed location";
feed = new Feed(url || DEFAULT_FEED_URL);
feed.download(false, false);
if (!feed.title)
feed.title = DEFAULT_FEED_TITLE;
var server = getIncomingServer();
var folder;
try {
//var folder = server.rootMsgFolder.FindSubFolder(feed.name);
folder = server.rootMsgFolder.getChildNamed(feed.name);
}
catch(e) {
// If we're here, it's probably because the folder doesn't exist yet,
// so create it.
debug("folder for new feed " + feed.title + " doesn't exist; creating");
server.rootMsgFolder.createSubfolder(feed.name, getMessageWindow());
folder = server.rootMsgFolder.FindSubFolder(feed.name);
var msgdb = folder.getMsgDatabase(null);
var folderInfo = msgdb.dBFolderInfo;
folderInfo.setCharPtrProperty("feedUrl", feed.url);
}
// XXX This should be something like "subscribe to feed".
dump ("feed name = " + feed.name + "\n");
addFeed(feed.url, feed.title, null, folder);
// XXX Maybe we can combine this with the earlier download?
feed.download();
}
function doEdit() {
// XXX There should be some way of correlating feed RDF resources
// with their corresponding Feed objects. Perhaps in the end much
// of this code could hang off methods of the Feed object.
var ds = getSubscriptionsDS();
var tree = document.getElementById('subscriptions');
var item = tree.view.getItemAtIndex(tree.view.selection.currentIndex);
var resource = rdf.GetResource(item.id);
var old_title = ds.GetTarget(resource, DC_TITLE, true);
old_title =
old_title ? old_title.QueryInterface(Components.interfaces.nsIRDFLiteral).Value : "";
var old_url = ds.GetTarget(resource, DC_IDENTIFIER, true);
old_url =
old_url ? old_url.QueryInterface(Components.interfaces.nsIRDFLiteral).Value : "";
new_title = window.prompt("Title:", old_title);
if (new_title == null)
return;
else if (new_title != old_title) {
var server = getIncomingServer();
var msgWindow = getMessageWindow();
// Throwing an error 0x80004005 seems to be getChildNamed()'s way
// of saying that a folder doesn't exist, so we need to trap that
// since it isn't fatal in our case. XXX We should probably check
// the error code in the catch statement to verify the problem.
var old_folder;
try {
old_folder = server.rootMsgFolder.getChildNamed(old_title);
old_folder = old_folder.QueryInterface(Components.interfaces.nsIMsgFolder);
} catch(e) {}
var new_folder;
try {
new_folder = server.rootMsgFolder.getChildNamed(new_title);
new_folder = new_folder.QueryInterface(Components.interfaces.nsIMsgFolder);
} catch(e) {}
if (old_folder && new_folder) {
// We could move messages from the old folder to the new folder
// and then delete the old folder (code for doing so is below),
// but how do we distinguish between messages in the old folder
// from this feed vs. from another feed? Until we can do that,
// which probably requires storing the feed ID in the message headers,
// we're better off just leaving the old folder as it is.
//var messages = old_folder.getMessages(msgWindow);
//var movees =
// Components
// .classes["@mozilla.org/supports-array;1"]
// .createInstance(Components.interfaces.nsISupportsArray);
//var message;
//while (messages.hasMoreElements()) {
// message = messages.getNext();
// movees.AppendElement(message);
//}
//gFoldersBeingDeleted.push(old_folder);
//new_folder.copyMessages(old_folder,
// movees,
// true /*isMove*/,
// msgWindow /* nsIMsgWindow */,
// null /* listener */,
// false /* isFolder */,
// false /*allowUndo*/ );
}
else if (old_folder) {
// We could rename the old folder to the new folder
// (code for doing so is below), but what if other feeds
// are using the old folder? Until we write code to determine
// whether they are or not (and perhaps even then), better to leave
// the old folder as it is and merely create a new folder.
//old_folder.rename(new_title, msgWindow);
server.rootMsgFolder.createSubfolder(new_title, msgWindow);
}
else if (new_folder) {
// Do nothing, as everything is as it should be.
}
else {
// Neither old nor new folders exist, so just create the new one.
server.rootMsgFolder.createSubfolder(new_title, msgWindow);
}
updateTitle(item.id, new_title);
}
new_url = window.prompt("Location:", old_url);
if (new_url == null) {
// The user cancelled the edit, but not until after potentially changing
// the title, so despite the cancellation we should still redownload
// the feed if the title has changed.
if (new_title != old_title) {
feed = new Feed(old_url, null, new_title);
feed.download();
}
return;
}
else if (new_url != old_url)
updateURL(item.id, new_url);
feed = new Feed(new_url, null, new_title);
feed.download();
//tree.builder.rebuild();
}
function doRemove() {
var tree = document.getElementById('subscriptions');
var item = tree.view.getItemAtIndex(tree.view.selection.currentIndex);
var resource = rdf.GetResource(item.id);
var ds = getSubscriptionsDS();
var feeds = getSubscriptionsList();
var index = feeds.IndexOf(resource);
if (index != -1) {
var title = ds.GetTarget(resource, DC_TITLE, true);
if (title) {
title = title.QueryInterface(Components.interfaces.nsIRDFLiteral);
// We could delete the folder, but what if other feeds are using it?
// XXX Should we check for other feeds using the folder and delete it
// if there aren't any? What if the user is using the folder
// for other purposes?
//var server = getIncomingServer();
//var openerResource = server.rootMsgFolder.QueryInterface(Components.interfaces.nsIRDFResource);
//var folderResource = server.rootMsgFolder.getChildNamed(title.Value).QueryInterface(Components.interfaces.nsIRDFResource);
//var foo = window.opener.messenger.DeleteFolders(window.opener.GetFolderDatasource(), openerResource, folderResource);
//try {
// // If the folder still exists, then it wasn't deleted,
// // which means the user answered "no" to the question of whether
// // they wanted to move the folder into the trash. That probably
// // means they changed their minds about removing the feed,
// // so don't remove it.
// folder = server.rootMsgFolder.getChildNamed(feed.name);
// if (folder) return;
//}
//catch (e) {}
ds.Unassert(resource, DC_TITLE, title, true);
}
var url = ds.GetTarget(resource, DC_IDENTIFIER, true);
if (url) {
url = url.QueryInterface(Components.interfaces.nsIRDFLiteral);
ds.Unassert(resource, DC_IDENTIFIER, url, true);
}
feeds.RemoveElementAt(index, true);
}
//tree.builder.rebuild();
}
/*
* Stuff
*/
function updateTitle(item, new_title) {
var ds = getSubscriptionsDS();
item = rdf.GetResource(item);
var old_title = ds.GetTarget(item, DC_TITLE, true);
if (old_title)
ds.Change(item, DC_TITLE, old_title, rdf.GetLiteral(new_title));
else
ds.Assert(item, DC_TITLE, rdf.GetLiteral(new_title), true);
}
function updateURL(item, new_url) {
var ds = getSubscriptionsDS();
var item = rdf.GetResource(item);
var old_url = ds.GetTarget(item, DC_IDENTIFIER, true);
if (old_url)
ds.Change(item, DC_IDENTIFIER, old_url, rdf.GetLiteral(new_url));
else
ds.Assert(item, DC_IDENTIFIER, rdf.GetLiteral(new_url), true);
}
function getIncomingServer() {
return window.opener.getIncomingServer();
}
function getMessageWindow() {
return window.opener.getMessageWindow();
}
/*
* Disabled/Future Stuff
*/
/*
var subscriptionsDragDropObserver = {
onDragStart: function (aEvent, aXferData, aDragAction) {
if (aEvent.originalTarget.localName != 'treechildren')
return;
var tree = document.getElementById('subscriptions');
var row = tree.treeBoxObject.selection.currentIndex;
window.status = "drag started at " + row;
aXferData.data = new TransferData();
aXferData.data.addDataForFlavour("text/unicode", row);
},
getSupportedFlavours : function () {
var flavours = new FlavourSet();
flavours.appendFlavour("text/unicode");
return flavours;
},
onDragOver: function (evt, flavour, session) {
var now = new Date();
//window.status = "dragging over " + now;
},
onDrop: function (evt, dropdata, session) {
var url = dropdata.data.slice(0, dropdata.data.indexOf(" "));
var name = dropdata.data.slice(dropdata.data.indexOf(" ")+1);
window.status = "dropping " + name + "; " + url;
subscribeToBlog(url, name);
}
};
var directoryDragObserver = {
onDragStart: function (aEvent, aXferData, aDragAction) {
if (aEvent.originalTarget.localName != 'treechildren')
return;
var tree = document.getElementById("directory");
var name = tree.view.getCellText(tree.currentIndex, "feeds-name-column");
var url = tree.view.getCellValue(tree.currentIndex, "feeds-name-column");
window.status = "dragging " + url + " " + name;
aXferData.data = new TransferData();
aXferData.data.addDataForFlavour("text/unicode", url + " " + name);
}
};
//var gFoldersBeingDeleted = [];
//var gFolderDeletionsCompleted = 0;
// Supposedly watches an asynchronous message copy, but doesn't seem
// to get called. I suspect the copy is actually synchronous, which is better
// in any case, since this listener doesn't actually know which folder
// it gets notifications about, so it has to rely on a very clunky mechanism
// for figuring out when it can delete the folders.
//var MessageCopyListener = {
// OnStartCopy: function() { alert('copy started'); },
// OnProgress: function() {},
// SetMessageKey: function() {},
// GetMessageId: function() {},
// OnStopCopy: function(aStatus, bar, baz) {
// alert(aStatus + " " + bar + " " + baz);
// ++gFolderDeletionsCompleted;
// if (gFoldersBeingDeleted.length == gFolderDeletionsCompleted) {
// for (i in gFoldersBeingDeleted)
// gFoldersBeingDeleted[i].Delete();
// gFoldersBeingDeleted = [];
// gFolderDeletionsCompleted = 0;
// }
// },
//}
*/

Просмотреть файл

@ -0,0 +1,72 @@
<?xml-stylesheet href="edittree.css" type="text/css"?>
<window id="subscriptionsDialog"
title="News &amp; Blog Feeds"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
width="400"
height="400"
persist="width height"
onload="doLoad();"
flex="1"
windowtype="mail:3pane">
<script type="application/x-javascript" src="utils.js" />
<script type="application/x-javascript" src="file-utils.js" />
<script type="application/x-javascript" src="debug-utils.js" />
<script type="application/x-javascript" src="subscriptions.js" />
<script type="application/x-javascript" src="Feed.js" />
<script type="application/x-javascript" src="FeedItem.js" />
<!-- The global Mozilla scripts that implement drag-and-drop support -->
<!--
<script src="chrome://global/content/nsDragAndDrop.js"/>
<script src="chrome://global/content/nsTransferable.js"/>
-->
<hbox flex="1">
<tree id="subscriptions" flex="1" seltype="single"
datasources="rdf:null"
style="width: 100%"
ref="urn:forumzilla:root"
onselect="this.treeBoxObject.view.selectionChanged();"
ondraggesture="//nsDragAndDrop.startDrag(event, subscriptionsDragDropObserver);"
ondragover="//nsDragAndDrop.dragOver(event, subscriptionsDragDropObserver);"
ondragdrop="//nsDragAndDrop.drop(event, subscriptionsDragDropObserver);">
<template>
<rule>
<conditions>
<content uri="?root" />
<triple subject="?root" predicate="urn:forumzilla:feeds" object="?feeds" />
<member container="?feeds" child="?feed" />
</conditions>
<bindings>
<binding subject="?feed" predicate="http://purl.org/dc/elements/1.1/title" object="?title" />
<binding subject="?feed" predicate="http://purl.org/dc/elements/1.1/identifier" object="?url" />
</bindings>
<action>
<treechildren flex="1">
<treeitem uri="?feed">
<treerow>
<treecell src="" label="?title"/>
<treecell src="" label="?url"/>
</treerow>
</treeitem>
</treechildren>
</action>
</rule>
</template>
<treecols>
<treecol id="subs-name-column" flex="1" label="Name" primary="true" />
<treecol id="subs-url-column" flex="1" label="Location" hidden="false" />
</treecols>
</tree>
<vbox>
<button label="Add" oncommand="doAdd();" />
<button label="Edit" oncommand="doEdit();" />
<button label="Remove" oncommand="doRemove();" />
<spring flex="1"/>
<button label="Done" oncommand="window.close();" />
</vbox>
</hbox>
</window>

Просмотреть файл

@ -0,0 +1,29 @@
<?xml version="1.0"?>
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/x-javascript">
function openSubscriptionsDialog() {
window.openDialog("chrome://messenger-newsblog/content/subscriptions.xul",
"News &amp; Blogs",
"centerscreen,resizable=yes");
}
</script>
<menupopup id="taskPopup">
<menuitem id="newsAndBlogsCmd"
label="News &amp; Blogs Settings..."
insertbefore="menu_preferences"
accesskey="e"
oncommand="openSubscriptionsDialog();"/>
</menupopup>
<toolbarpalette id="MailToolbarPalette">
<toolbarbutton id="button-newsandblogs"
label="News &amp; Blogs"
tooltiptext="manage news and blog feeds"
oncommand="openSubscriptionsDialog();"/>
</toolbarpalette>
</overlay>

Просмотреть файл

@ -0,0 +1,186 @@
// XXX Rename this to global.js
// Whether or not to dump debugging messages to the console.
const DEBUG = true;
var debug;
if (DEBUG)
debug = function(msg) { dump(' -- FZ -- : ' + msg + '\n'); }
else
debug = function() {}
var rdf =
Components
.classes["@mozilla.org/rdf/rdf-service;1"]
.getService(Components.interfaces.nsIRDFService);
const RDF_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
const RDF_TYPE = rdf.GetResource(RDF_NS + "type");
const RSS_NS = "http://purl.org/rss/1.0/";
const RSS_CHANNEL = rdf.GetResource(RSS_NS + "channel");
const RSS_TITLE = rdf.GetResource(RSS_NS + "title");
const RSS_DESCRIPTION = rdf.GetResource(RSS_NS + "description");
const RSS_ITEMS = rdf.GetResource(RSS_NS + "items");
const RSS_ITEM = rdf.GetResource(RSS_NS + "item");
const RSS_LINK = rdf.GetResource(RSS_NS + "link");
const RSS_CONTENT_NS = "http://purl.org/rss/1.0/modules/content/";
const RSS_CONTENT_ENCODED = rdf.GetResource(RSS_CONTENT_NS + "encoded");
const DC_NS = "http://purl.org/dc/elements/1.1/";
const DC_CREATOR = rdf.GetResource(DC_NS + "creator");
const DC_SUBJECT = rdf.GetResource(DC_NS + "subject");
const DC_DATE = rdf.GetResource(DC_NS + "date");
const DC_TITLE = rdf.GetResource(DC_NS + "title");
const DC_IDENTIFIER = rdf.GetResource(DC_NS + "identifier");
const FZ_NS = "urn:forumzilla:";
const FZ_ROOT = rdf.GetResource(FZ_NS + "root");
const FZ_FEEDS = rdf.GetResource(FZ_NS + "feeds");
const FZ_FEED = rdf.GetResource(FZ_NS + "feed");
const FZ_QUICKMODE = rdf.GetResource(FZ_NS + "quickMode");
const FZ_DESTFOLDER = rdf.GetResource(FZ_NS + "destFolder");
// XXX There's a containerutils in forumzilla.js that this should be merged with.
var containerUtils =
Components
.classes["@mozilla.org/rdf/container-utils;1"]
.getService(Components.interfaces.nsIRDFContainerUtils);
var fileHandler =
Components
.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService)
.getProtocolHandler("file")
.QueryInterface(Components.interfaces.nsIFileProtocolHandler);
function addFeed(url, title, quickMode, destFolder) {
var ds = getSubscriptionsDS();
var feeds = getSubscriptionsList();
// Give quickMode a default value of "true"; otherwise convert value
// to either "true" or "false" string.
quickMode = quickMode == null ? "false" : quickMode ? "true" : "false";
// Generate a unique ID for the feed.
var id = url;
var i = 1;
while (feeds.IndexOf(rdf.GetResource(id)) != -1 && ++i < 1000)
id = url + i;
if (id == 1000)
throw("couldn't generate a unique ID for feed " + url);
// Add the feed to the list.
id = rdf.GetResource(id);
feeds.AppendElement(id);
ds.Assert(id, RDF_TYPE, FZ_FEED, true);
ds.Assert(id, DC_IDENTIFIER, rdf.GetLiteral(url), true);
if (title)
ds.Assert(id, DC_TITLE, rdf.GetLiteral(title), true);
ds.Assert(id, FZ_QUICKMODE, rdf.GetLiteral(quickMode), true);
ds.Assert(id, FZ_DESTFOLDER, destFolder, true);
ds = ds.QueryInterface(Components.interfaces.nsIRDFRemoteDataSource);
ds.Flush();
}
function getNodeValue(node) {
if (node && node.textContent)
return node.textContent;
else if (node && node.firstChild) {
var ret = "";
for (var child = node.firstChild; child; child = child.nextSibling) {
var value = getNodeValue(child);
if (value)
ret += value;
}
if (ret)
return ret;
}
return null;
}
function getRDFTargetValue(ds, source, property) {
var node = ds.GetTarget(source, property, true);
if (node) {
node = node.QueryInterface(Components.interfaces.nsIRDFLiteral);
if (node)
return node.Value;
}
return null;
}
var gFzSubscriptionsDS; // cache
function getSubscriptionsDS() {
if (gFzSubscriptionsDS)
return gFzSubscriptionsDS;
var file = getSubscriptionsFile();
var url = fileHandler.getURLSpecFromFile(file);
gFzSubscriptionsDS = rdf.GetDataSource(url);
if (!gFzSubscriptionsDS)
throw("can't get subscriptions data source");
// 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.
return gFzSubscriptionsDS;
}
function getSubscriptionsList() {
var ds = getSubscriptionsDS();
var list = ds.GetTarget(FZ_ROOT, FZ_FEEDS, true);
//list = feeds.QueryInterface(Components.interfaces.nsIRDFContainer);
list = list.QueryInterface(Components.interfaces.nsIRDFResource);
list = containerUtils.MakeSeq(ds, list);
return list;
}
function getSubscriptionsFile() {
// Get the app directory service so we can look up the user's profile dir.
var appDirectoryService =
Components
.classes["@mozilla.org/file/directory_service;1"]
.getService(Components.interfaces.nsIProperties);
if ( !appDirectoryService )
throw("couldn't get the directory service");
// Get the user's profile directory.
var profileDir =
appDirectoryService.get("ProfD", Components.interfaces.nsIFile);
if ( !profileDir )
throw ("couldn't get the user's profile directory");
// Get the user's subscriptions file.
var file = profileDir.clone();
file.append("feeds.rdf");
// If the file doesn't exist, create it.
if (!file.exists())
createSubscriptionsFile(file);
return file;
}
function createSubscriptionsFile(file) {
file = new LocalFile(file, MODE_WRONLY | MODE_CREATE);
file.write('\
<?xml version="1.0"?>\n\
<RDF:RDF xmlns:dc="http://purl.org/dc/elements/1.1/"\n\
xmlns:fz="' + FZ_NS + '"\n\
xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">\n\
<RDF:Description about="' + FZ_ROOT.Value + '">\n\
<fz:feeds>\n\
<RDF:Seq>\n\
</RDF:Seq>\n\
</fz:feeds>\n\
</RDF:Description>\n\
</RDF:RDF>\n\
');
file.close();
}

17
mail/extensions/newsblog/jar.mn Executable file
Просмотреть файл

@ -0,0 +1,17 @@
newsblog.jar:
* content/messenger-newsblog/toolbar-icon.xul (content/toolbar-icon.xul)
* content/messenger-newsblog/forumzilla.xul (content/forumzilla.xul)
* content/messenger-newsblog/forumzilla.js (content/forumzilla.js)
* content/messenger-newsblog/subscriptions.xul (content/subscriptions.xul)
* content/messenger-newsblog/edittree.xml (content/edittree.xml)
* content/messenger-newsblog/debug-utils.js (content/debug-utils.js)
* content/messenger-newsblog/Feed.js (content/Feed.js)
* content/messenger-newsblog/FeedItem.js (content/FeedItem.js)
* content/messenger-newsblog/file-utils.js (content/file-utils.js)
* content/messenger-newsblog/subscriptions.js (content/subscriptions.js)
* content/messenger-newsblog/utils.js (content/utils.js)
* content/messenger-newsblog/contents.rdf (content/contents.rdf)
newsblog.jar:
locale/en-US/messenger-newsblog/newsblog.dtd (locale/newsblog.dtd)
* locale/en-US/messenger-newsblog/contents.rdf (locale/contents.rdf)

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

Просмотреть файл

@ -0,0 +1 @@
<!ENTITY newsBlogMenu.label "News &amp; Blogs">

Просмотреть файл

@ -0,0 +1,25 @@
#
# The contents of this file are subject to the Netscape 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/NPL/
#
# 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 mozilla.org code.
#
#
# Contributor(s):
#
DEPTH = ../../../..
topsrcdir = @top_srcdir@
srcdir = @srcdir@
VPATH = @srcdir@
include $(DEPTH)/config/autoconf.mk
include $(topsrcdir)/config/rules.mk

Просмотреть файл

Просмотреть файл

@ -0,0 +1,6 @@
#button-newsandblogs {
-moz-box-orient:vertical;
/* from http://www.aaa-clipart.com/data/icons/mail/mail1.gif */
list-style-image: url("chrome://forumzilla/content/icon.gif");
}