!import
1 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 * ***** BEGIN LICENSE BLOCK *****
3 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
4 *
5 * The contents of this file are subject to the Mozilla Public License Version
6 * 1.1 (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 * http://www.mozilla.org/MPL/
9 *
10 * Software distributed under the License is distributed on an "AS IS" basis,
11 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12 * for the specific language governing rights and limitations under the
13 * License.
14 *
15 * The Original Code is the News&Blog Feed Downloader
16 *
17 * The Initial Developer of the Original Code is
18 * The Mozilla Foundation.
19 * Portions created by the Initial Developer are Copyright (C) 2004
20 * the Initial Developer. All Rights Reserved.
21 *
22 * Contributor(s):
23 * Myk Melez <myk@mozilla.org) (Original Author)
24 * David Bienvenu <bienvenu@nventure.com>
25 *
26 * Alternatively, the contents of this file may be used under the terms of
27 * either the GNU General Public License Version 2 or later (the "GPL"), or
28 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
29 * in which case the provisions of the GPL or the LGPL are applicable instead
30 * of those above. If you wish to allow use of your version of this file only
31 * under the terms of either the GPL or the LGPL, and not to allow others to
32 * use your version of this file under the terms of the MPL, indicate your
33 * decision by deleting the provisions above and replace them with the notice
34 * and other provisions required by the GPL or the LGPL. If you do not delete
35 * the provisions above, a recipient may use your version of this file under
36 * the terms of any one of the MPL, the GPL or the LGPL.
37 *
38 * ***** END LICENSE BLOCK ***** */
39
40 var gExternalScriptsLoaded = false;
41
42 var nsNewsBlogFeedDownloader =
43 {
downloadFeed
44 downloadFeed: function(aUrl, aFolder, aQuickMode, aTitle, aUrlListener, aMsgWindow)
45 {
46 if (!gExternalScriptsLoaded)
47 loadScripts();
48
49 // we don't yet support the ability to check for new articles while we are in the middle of
50 // subscribing to a feed. For now, abort the check for new feeds.
51 if (progressNotifier.mSubscribeMode)
52 {
53 debug('Aborting RSS New Mail Check. Feed subscription in progress\n');
54 return;
55 }
56 // if folder seems to have lost its feeds, look in DS for feeds.
57 if (!aUrl.length)
58 {
59 var ds = getSubscriptionsDS(aFolder.server);
60 var enumerator = ds.GetSources(FZ_DESTFOLDER, aFolder, true);
61 var concatenatedUris = "";
62 while (enumerator.hasMoreElements())
63 {
64 var containerArc = enumerator.getNext();
65 var uri = containerArc.QueryInterface(Components.interfaces.nsIRDFResource).Value;
66 if (concatenatedUris.length > 0)
67 concatenatedUris += "|";
68 concatenatedUris += uri;
69 }
70 if (concatenatedUris.length > 0)
71 {
72 aUrl = concatenatedUris;
73 try
74 {
75 var msgdb = aFolder.getMsgDatabase(null);
76 var folderInfo = msgdb.dBFolderInfo;
77 folderInfo.setCharProperty("feedUrl", concatenatedUris);
78 }
79 catch (ex) {dump(ex);}
80 }
81 }
82 // aUrl may be a delimited list of feeds for a particular folder. We need to kick off a download
83 // for each feed.
84
85 var feedUrlArray = aUrl.split("|");
86
87 // we might just pull all these args out of the aFolder DB, instead of passing them in...
88 var rdf = Components.classes["@mozilla.org/rdf/rdf-service;1"]
89 .getService(Components.interfaces.nsIRDFService);
90
91 progressNotifier.init(aMsgWindow, false);
92
93 for (url in feedUrlArray)
94 {
95 if (feedUrlArray[url])
96 {
97 id = rdf.GetResource(feedUrlArray[url]);
98 feed = new Feed(id, aFolder.server);
99 feed.folder = aFolder;
100 gNumPendingFeedDownloads++; // bump our pending feed download count
101 feed.download(true, progressNotifier);
102 }
103 }
104 },
105
subscribeToFeed
106 subscribeToFeed: function(aUrl, aFolder, aMsgWindow)
107 {
108 if (!gExternalScriptsLoaded)
109 loadScripts();
110
111 // we don't support the ability to subscribe to several feeds at once yet...
112 // for now, abort the subscription if we are already in the middle of subscribing to a feed
113 // via drag and drop.
114 if (gNumPendingFeedDownloads)
115 {
116 debug('Aborting RSS subscription. Feed downloads already in progress\n');
117 return;
118 }
119
120 // if aFolder is null, then use the root folder for the first RSS account
121 if (!aFolder)
122 {
123 var accountManager = Components.classes["@mozilla.org/messenger/account-manager;1"]
124 .getService(Components.interfaces.nsIMsgAccountManager);
125 var allServers = accountManager.allServers;
126 for (var i=0; i< allServers.Count() && !aFolder; i++)
127 {
128 var currentServer = allServers.GetElementAt(i).QueryInterface(Components.interfaces.nsIMsgIncomingServer);
129 if (currentServer && currentServer.type == 'rss')
130 aFolder = currentServer.rootFolder;
131 }
132 }
133
134 // What do we do if the user hasn't created an RSS account yet?
135 // for now, fall out, would be nice if we could force RSS account creation
136 if (!aFolder)
137 return;
138
139 // if aUrl is a feed url, then it is of the form: feed:http://somesite/feed.xml or
140 // feed://http://somesite/feed.xml
141 // Strip off the feed: part so we can subscribe to the contained URL.
142 if (/^feed:/i.test(aUrl))
143 {
144 aUrl = aUrl.replace(/^feed:/i, '');
145 // Strip off the optional forward slashes if we were given feed://
146 aUrl = aUrl.replace(/^\x2f\x2f/, '');
147 }
148
149 // make sure we aren't already subscribed to this feed before we attempt to subscribe to it.
150 if (feedAlreadyExists(aUrl, aFolder.server))
151 {
152 aMsgWindow.statusFeedback.showStatusString(GetNewsBlogStringBundle().GetStringFromName('subscribe-feedAlreadySubscribed'));
153 return;
154 }
155
156 var rdf = Components.classes["@mozilla.org/rdf/rdf-service;1"]
157 .getService(Components.interfaces.nsIRDFService);
158
159 var itemResource = rdf.GetResource(aUrl);
160 var feed = new Feed(itemResource, aFolder.server);
161 feed.quickMode = feed.server.getBoolAttribute('quickMode');
162
163 if (!aFolder.isServer) // if the root server, create a new folder for the feed
164 feed.folder = aFolder; // user must want us to add this subscription url to an existing RSS folder.
165
166 progressNotifier.init(aMsgWindow, true);
167 gNumPendingFeedDownloads++;
168 feed.download(true, progressNotifier);
169 },
170
updateSubscriptionsDS
171 updateSubscriptionsDS: function(aFolder, aUnsubscribe)
172 {
173 if (!gExternalScriptsLoaded)
174 loadScripts();
175
176 // an rss folder was just renamed...we need to update our feed data source
177 var msgdb = aFolder.QueryInterface(Components.interfaces.nsIMsgFolder).getMsgDatabase(null);
178 var folderInfo = msgdb.dBFolderInfo;
179 var feedurls = folderInfo.getCharProperty("feedUrl");
180 var feedUrlArray = feedurls.split("|");
181
182 var rdf = Components.classes["@mozilla.org/rdf/rdf-service;1"].getService(Components.interfaces.nsIRDFService);
183 var ds = getSubscriptionsDS(aFolder.server);
184
185 for (url in feedUrlArray)
186 {
187 if (feedUrlArray[url])
188 {
189 id = rdf.GetResource(feedUrlArray[url]);
190 // get the node for the current folder URI
191 var node = ds.GetTarget(id, FZ_DESTFOLDER, true);
192
193 // we need to check and see if the folder is a child of the trash...if it is, then we can
194 // treat this as an unsubscribe action
195 if (aUnsubscribe)
196 {
197 var feeds = getSubscriptionsList(aFolder.server);
198 var index = feeds.IndexOf(id);
199 if (index != -1)
200 feeds.RemoveElementAt(index, false);
201 removeAssertions(ds, id);
202 }
203 else
204 ds.Change(id, FZ_DESTFOLDER, node, rdf.GetResource(aFolder.URI));
205 }
206 } // for each feed url in the folder property
207
208 ds.QueryInterface(Components.interfaces.nsIRDFRemoteDataSource).Flush(); // flush any changes
209 },
210
QueryInterface
211 QueryInterface: function(aIID)
212 {
213 if (aIID.equals(Components.interfaces.nsINewsBlogFeedDownloader) ||
214 aIID.equals(Components.interfaces.nsISupports))
215 return this;
216
217 Components.returnCode = Components.results.NS_ERROR_NO_INTERFACE;
218 return null;
219 }
220 }
221
222 var nsNewsBlogAcctMgrExtension =
223 {
224 name: "newsblog",
225 chromePackageName: "messenger-newsblog",
showPanel
226 showPanel: function (server)
227 {
228 return server.type == "rss";
229 },
QueryInterface
230 QueryInterface: function(aIID)
231 {
232 if (aIID.equals(Components.interfaces.nsIMsgAccountManagerExtension) ||
233 aIID.equals(Components.interfaces.nsISupports))
234 return this;
235
236 Components.returnCode = Components.results.NS_ERROR_NO_INTERFACE;
237 return null;
238 }
239 }
240
241 var nsFeedCommandLineHandler =
242 {
243 /* nsISupports */
QueryInterface
244 QueryInterface : function(aIID)
245 {
246 if (!aIID.equals(Components.interfaces.nsISupports) &&
247 !aIID.equals(Components.interfaces.nsICommandLineHandler))
248 throw Components.results.NS_ERROR_NO_INTERFACE;
249 return this;
250 },
251
252 /* nsICommandLineHandler */
handle
253 handle : function(cmdLine)
254 {
255 // we only care about "-mail someurl" where someurl is a feed: url
256 // we also don't want to remove the parameter in case we don't end up handling it...
257
258 var mailPos = cmdLine.findFlag("mail", false);
259 if (mailPos != -1 && cmdLine.length >= mailPos )
260 {
261 var uriStr = cmdLine.getArgument(mailPos + 1);
262 if (/^feed:/i.test(uriStr))
263 {
264 var mailWindow = Components.classes["@mozilla.org/appshell/window-mediator;1"].getService()
265 .QueryInterface(Components.interfaces.nsIWindowMediator).getMostRecentWindow("mail:3pane");
266
267 // if we don't have a 3 pane window visible already, then we can optimize and do nothing here,
268 // that will let the default command line handler create a 3pane window for us using the feed
269 // URL as an argument to that window when it gets constructed...so we only care about the
270 // case where we want to re-use an existing 3 pane to subscribe to the feed url
271 if (mailWindow)
272 {
273 cmdLine.handleFlagWithParam("mail", false); // eat up the arguments we are now handling
274 cmdLine.preventDefault = true; // prevent the default cmd line handler from doing anything
275
276 var feedHandler = Components.classes["@mozilla.org/newsblog-feed-downloader;1"].getService(Components.interfaces.nsINewsBlogFeedDownloader);
277 if (feedHandler)
278 feedHandler.subscribeToFeed(uriStr, null, mailWindow.msgWindow);
279 }
280 }
281 }
282 },
283
284 helpInfo : ""
285 };
286
287 var nsNewsBlogFeedDownloaderModule =
288 {
getClassObject
289 getClassObject: function(aCompMgr, aCID, aIID)
290 {
291 if (!aIID.equals(Components.interfaces.nsIFactory))
292 throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
293
294 for (var key in this.mObjects)
295 if (aCID.equals(this.mObjects[key].CID))
296 return this.mObjects[key].factory;
297
298 throw Components.results.NS_ERROR_NO_INTERFACE;
299 },
300
301 mObjects:
302 {
303 feedDownloader:
304 {
305 CID: Components.ID("{5c124537-adca-4456-b2b5-641ab687d1f6}"),
306 contractID: "@mozilla.org/newsblog-feed-downloader;1",
307 className: "News+Blog Feed Downloader",
308 factory:
309 {
createInstance
310 createInstance: function (aOuter, aIID)
311 {
312 if (aOuter != null)
313 throw Components.results.NS_ERROR_NO_AGGREGATION;
314 if (!aIID.equals(Components.interfaces.nsINewsBlogFeedDownloader) &&
315 !aIID.equals(Components.interfaces.nsISupports))
316 throw Components.results.NS_ERROR_INVALID_ARG;
317
318 // return the singleton
319 return nsNewsBlogFeedDownloader.QueryInterface(aIID);
320 }
321 } // factory
322 }, // feed downloader
323
324 nsNewsBlogAcctMgrExtension:
325 {
326 CID: Components.ID("{E109C05F-D304-4ca5-8C44-6DE1BFAF1F74}"),
327 contractID: "@mozilla.org/accountmanager/extension;1?name=newsblog",
328 className: "News+Blog Account Manager Extension",
329 factory:
330 {
createInstance
331 createInstance: function (aOuter, aIID)
332 {
333 if (aOuter != null)
334 throw Components.results.NS_ERROR_NO_AGGREGATION;
335 if (!aIID.equals(Components.interfaces.nsIMsgAccountManagerExtension) &&
336 !aIID.equals(Components.interfaces.nsISupports))
337 throw Components.results.NS_ERROR_INVALID_ARG;
338
339 // return the singleton
340 return nsNewsBlogAcctMgrExtension.QueryInterface(aIID);
341 }
342 } // factory
343 }, // account manager extension
344
345 nsFeedCommandLineHandler:
346 {
347 CID: Components.ID("{0E377BF7-E4FE-4c94-804C-0C33D49F883E}"),
348 contractID: "@mozilla.org/newsblog-feed-downloader/clh;1",
349 className: "Feed CommandLine Handler",
350 factory:
351 {
createInstance
352 createInstance: function (aOuter, aIID)
353 {
354 if (aOuter != null)
355 throw Components.results.NS_ERROR_NO_AGGREGATION;
356 if (!aIID.equals(Components.interfaces.nsICommandLineHandler) &&
357 !aIID.equals(Components.interfaces.nsISupports))
358 throw Components.results.NS_ERROR_INVALID_ARG;
359
360 // return the singleton
361 return nsFeedCommandLineHandler.QueryInterface(aIID);
362 }
363 } // factory
364 }
365 },
366
registerSelf
367 registerSelf: function(aCompMgr, aFileSpec, aLocation, aType)
368 {
369 aCompMgr = aCompMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
370 for (var key in this.mObjects)
371 {
372 var obj = this.mObjects[key];
373 aCompMgr.registerFactoryLocation(obj.CID, obj.className, obj.contractID, aFileSpec, aLocation, aType);
374 }
375
376 // we also need to do special account extension registration
377 var catman = Components.classes["@mozilla.org/categorymanager;1"].getService(Components.interfaces.nsICategoryManager);
378 catman.addCategoryEntry("mailnews-accountmanager-extensions",
379 "newsblog account manager extension",
380 "@mozilla.org/accountmanager/extension;1?name=newsblog", true, true);
381 catman.addCategoryEntry("command-line-handler",
382 "l-feed",
383 "@mozilla.org/newsblog-feed-downloader/clh;1", true, true);
384 },
385
unregisterSelf
386 unregisterSelf: function(aCompMgr, aFileSpec, aLocation)
387 {
388 aCompMgr = aCompMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
389 for (var key in this.mObjects)
390 {
391 var obj = this.mObjects[key];
392 aCompMgr.unregisterFactoryLocation(obj.CID, aFileSpec);
393 }
394
395 // unregister the account manager extension
396 catman = Components.classes["@mozilla.org/categorymanager;1"].getService(Components.interfaces.nsICategoryManager);
397 catman.deleteCategoryEntry("mailnews-accountmanager-extensions",
398 "@mozilla.org/accountmanager/extension;1?name=newsblog", true);
399 catMan.addCategoryEntry("command-line-handler",
400 "@mozilla.org/newsblog-feed-downloader/clh;1", true);
401 },
402
canUnload
403 canUnload: function(aCompMgr)
404 {
405 return true;
406 }
407 };
408
NSGetModule
409 function NSGetModule(aCompMgr, aFileSpec)
410 {
411 return nsNewsBlogFeedDownloaderModule;
412 }
413
loadScripts
414 function loadScripts()
415 {
416 var scriptLoader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
417 .getService(Components.interfaces.mozIJSSubScriptLoader);
418 if (scriptLoader)
419 {
420 scriptLoader.loadSubScript("chrome://messenger-newsblog/content/Feed.js");
421 scriptLoader.loadSubScript("chrome://messenger-newsblog/content/FeedItem.js");
422 scriptLoader.loadSubScript("chrome://messenger-newsblog/content/feed-parser.js");
423 scriptLoader.loadSubScript("chrome://messenger-newsblog/content/file-utils.js");
424 scriptLoader.loadSubScript("chrome://messenger-newsblog/content/utils.js");
425 }
426
427 gExternalScriptsLoaded = true;
428 }
429
430 // Progress glue code. Acts as a go between the RSS back end and the mail window front end
431 // determined by the aMsgWindow parameter passed into nsINewsBlogFeedDownloader.
432 // gNumPendingFeedDownloads: keeps track of the total number of feeds we have been asked to download
433 // this number may not reflect the # of entries in our mFeeds array because not all
434 // feeds may have reported in for the first time...
435 var gNumPendingFeedDownloads = 0;
436
437 var progressNotifier = {
438 mSubscribeMode: false,
439 mMsgWindow: null,
440 mStatusFeedback: null,
441 mFeeds: new Array,
442
init
443 init: function(aMsgWindow, aSubscribeMode)
444 {
445 if (!gNumPendingFeedDownloads) // if we aren't already in the middle of downloading feed items...
446 {
447 this.mStatusFeedback = aMsgWindow ? aMsgWindow.statusFeedback : null;
448 this.mSubscribeMode = aSubscribeMode;
449 this.mMsgWindow = aMsgWindow;
450
451 if (this.mStatusFeedback)
452 {
453 this.mStatusFeedback.startMeteors();
454 this.mStatusFeedback.showStatusString(aSubscribeMode ? GetNewsBlogStringBundle().GetStringFromName('subscribe-validating')
455 : GetNewsBlogStringBundle().GetStringFromName('newsblog-getNewMailCheck'));
456 }
457 }
458 },
459
downloaded
460 downloaded: function(feed, aErrorCode)
461 {
462 if (this.mSubscribeMode && aErrorCode == kNewsBlogSuccess)
463 {
464 // if we get here...we should always have a folder by now...either
465 // in feed.folder or FeedItems created the folder for us....
466 updateFolderFeedUrl(feed.folder, feed.url, false);
467 addFeed(feed.url, feed.name, feed.folder); // add feed just adds the feed to the subscription UI and flushes the datasource
468
469 // Nice touch: select the folder that now contains the newly subscribed feed...this is particularly nice
470 // if we just finished subscribing to a feed URL that the operating system gave us.
471 this.mMsgWindow.windowCommands.selectFolder(feed.folder.URI);
472 }
473 else if (feed.folder)
474 feed.folder.setMsgDatabase(null);
475
476 if (this.mStatusFeedback)
477 {
478 var newsBlogBundle = GetNewsBlogStringBundle();
479 if (aErrorCode == kNewsBlogNoNewItems)
480 this.mStatusFeedback.showStatusString(newsBlogBundle.GetStringFromName("newsblog-noNewArticlesForFeed"));
481 else if (aErrorCode == kNewsBlogInvalidFeed)
482 this.mStatusFeedback.showStatusString(newsBlogBundle.formatStringFromName("newsblog-invalidFeed",
483 [feed.url], 1));
484 else if (aErrorCode == kNewsBlogRequestFailure)
485 this.mStatusFeedback.showStatusString(newsBlogBundle.formatStringFromName("newsblog-networkError",
486 [feed.url], 1));
487 this.mStatusFeedback.stopMeteors();
488 }
489
490 gNumPendingFeedDownloads--;
491
492 if (!gNumPendingFeedDownloads)
493 {
494 this.mFeeds = new Array;
495
496 this.mSubscribeMode = false;
497
498 // should we do this on a timer so the text sticks around for a little while?
499 // It doesnt look like we do it on a timer for newsgroups so we'll follow that model.
500 if (aErrorCode == kNewsBlogSuccess && this.mStatusFeedback) // don't clear the status text if we just dumped an error to the status bar!
501 this.mStatusFeedback.showStatusString("");
502 }
503 },
504
505 // this gets called after the RSS parser finishes storing a feed item to disk
506 // aCurrentFeedItems is an integer corresponding to how many feed items have been downloaded so far
507 // aMaxFeedItems is an integer corresponding to the total number of feed items to download
onFeedItemStored
508 onFeedItemStored: function (feed, aCurrentFeedItems, aMaxFeedItems)
509 {
510 // we currently don't do anything here. Eventually we may add
511 // status text about the number of new feed articles received.
512
513 if (this.mSubscribeMode && this.mStatusFeedback) // if we are subscribing to a feed, show feed download progress
514 {
515 this.mStatusFeedback.showStatusString(GetNewsBlogStringBundle().formatStringFromName("subscribe-fetchingFeedItems", [aCurrentFeedItems, aMaxFeedItems], 2));
516 this.onProgress(feed, aCurrentFeedItems, aMaxFeedItems);
517 }
518 },
519
onProgress
520 onProgress: function(feed, aProgress, aProgressMax)
521 {
522 if (feed.url in this.mFeeds) // have we already seen this feed?
523 this.mFeeds[feed.url].currentProgress = aProgress;
524 else
525 this.mFeeds[feed.url] = {currentProgress: aProgress, maxProgress: aProgressMax};
526
527 this.updateProgressBar();
528 },
529
updateProgressBar
530 updateProgressBar: function()
531 {
532 var currentProgress = 0;
533 var maxProgress = 0;
534 for (index in this.mFeeds)
535 {
536 currentProgress += this.mFeeds[index].currentProgress;
537 maxProgress += this.mFeeds[index].maxProgress;
538 }
539
540 // if we start seeing weird "jumping" behavior where the progress bar goes below a threshold then above it again,
541 // then we can factor a fudge factor here based on the number of feeds that have not reported yet and the avg
542 // progress we've already received for existing feeds. Fortunately the progressmeter is on a timer
543 // and only updates every so often. For the most part all of our request have initial progress
544 // before the UI actually picks up a progress value.
545
546 if (this.mStatusFeedback)
547 {
548 var progress = (currentProgress * 100) / maxProgress;
549 this.mStatusFeedback.showProgress(progress);
550 }
551 }
552 }
553
GetNewsBlogStringBundle
554 function GetNewsBlogStringBundle(name)
555 {
556 var strBundleService = Components.classes["@mozilla.org/intl/stringbundle;1"].getService();
557 strBundleService = strBundleService.QueryInterface(Components.interfaces.nsIStringBundleService);
558 var strBundle = strBundleService.createBundle("chrome://messenger-newsblog/locale/newsblog.properties");
559 return strBundle;
560 }