зеркало из https://github.com/mozilla/pjs.git
initial checkin of native thunderbird rss extension, NPOB, r/sr=sspitzer
This commit is contained in:
Родитель
657ad8705e
Коммит
275d570d89
|
@ -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(/</g, "<");
|
||||
content = content.replace(/>/g, ">");
|
||||
content = content.replace(/&/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 & 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 & Blogs",
|
||||
"centerscreen,resizable=yes");
|
||||
}
|
||||
</script>
|
||||
|
||||
<menupopup id="taskPopup">
|
||||
<menuitem id="newsAndBlogsCmd"
|
||||
label="News & Blogs Settings..."
|
||||
insertbefore="menu_preferences"
|
||||
accesskey="e"
|
||||
oncommand="openSubscriptionsDialog();"/>
|
||||
</menupopup>
|
||||
|
||||
<toolbarpalette id="MailToolbarPalette">
|
||||
<toolbarbutton id="button-newsandblogs"
|
||||
label="News & 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();
|
||||
}
|
|
@ -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 & 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");
|
||||
}
|
||||
|
Загрузка…
Ссылка в новой задаче