!import
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
1 //@line 37 "/home/visbrero/mnt/roisin/rev_control/hg/mozilla/mail/extensions/newsblog/content/Feed.js"
2
3
4 // error codes used to inform the consumer about attempts to download a feed
5 const kNewsBlogSuccess = 0;
6 const kNewsBlogInvalidFeed = 1; // usually means there was an error trying to parse the feed...
7 const kNewsBlogRequestFailure = 2; // generic networking failure when trying to download the feed.
8 const kNewsBlogFeedIsBusy = 3;
9 const kNewsBlogNoNewItems = 4; // there are no new articles for this feed
10
11 // Cache for all of the feeds currently being downloaded, indexed by URL, so the load event listener
12 // can access the Feed objects after it finishes downloading the feed.
13 var FeedCache =
14 {
15 mFeeds: new Array(),
16
putFeed
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
17 putFeed: function (aFeed)
18 {
19 this.mFeeds[this.normalizeHost(aFeed.url)] = aFeed;
20 },
21
getFeed
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
22 getFeed: function (aUrl)
23 {
24 return this.mFeeds[this.normalizeHost(aUrl)];
25 },
26
removeFeed
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
27 removeFeed: function (aUrl)
28 {
29 delete this.mFeeds[this.normalizeHost(aUrl)];
30 },
31
normalizeHost
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
32 normalizeHost: function (aUrl)
33 {
34 var ioService = Components.classes["@mozilla.org/network/io-service;1"].
35 getService(Components.interfaces.nsIIOService);
36 var normalizedUrl = ioService.newURI(aUrl, null, null);
37 normalizedUrl.host = normalizedUrl.host.toLowerCase();
38 return normalizedUrl.spec;
39 }
40 };
41
Feed
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
42 function Feed(aResource, aRSSServer)
43 {
44 this.resource = aResource.QueryInterface(Components.interfaces.nsIRDFResource);
45 this.server = aRSSServer;
46 }
47
48 Feed.prototype =
49 {
50 description: null,
51 author: null,
52 request: null,
53 server: null,
54 downloadCallback: null,
55 resource: null,
56 items: new Array(),
57 mFolder: null,
58 mInvalidFeed: false,
59
get_folder
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
60 get folder()
61 {
62 if (!this.mFolder)
63 {
64 try
65 {
66 this.mFolder = this.server.rootMsgFolder.getChildNamed(this.name);
67 } catch (ex) {}
68 }
69
70 return this.mFolder;
71 },
72
set_folder
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
73 set folder (aFolder)
74 {
75 this.mFolder = aFolder;
76 },
77
get_name
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
78 get name()
79 {
80 var name = this.title || this.description || this.url;
81 if (!name)
82 throw("couldn't compute feed name, as feed has no title, description, or URL.");
83
84 // Make sure the feed name doesn't have any line breaks, since we're going
85 // to use it as the name of the folder in the filesystem. This may not
86 // be necessary, since Mozilla's mail code seems to handle other forbidden
87 // characters in filenames and can probably handle these as well.
88 name = name.replace(/[\n\r\t]+/g, " ");
89
90 // Make sure the feed doesn't end in a period to work around bug 117840.
91 name = name.replace(/\.+$/, "");
92
93 return name;
94 },
95
download
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
96 download: function(aParseItems, aCallback)
97 {
98 this.downloadCallback = aCallback; // may be null
99
100 // Whether or not to parse items when downloading and parsing the feed.
101 // Defaults to true, but setting to false is useful for obtaining
102 // just the title of the feed when the user subscribes to it.
103 this.parseItems = aParseItems == null ? true : aParseItems ? true : false;
104
105 // Before we do anything...make sure the url is an http url. This is just a sanity check
106 // so we don't try opening mailto urls, imap urls, etc. that the user may have tried to subscribe to
107 // as an rss feed..
108 var uri = Components.classes["@mozilla.org/network/standard-url;1"].
109 createInstance(Components.interfaces.nsIURI);
110 uri.spec = this.url;
111 if (!(uri.schemeIs("http") || uri.schemeIs("https")))
112 {
113 this.onParseError(this); // simulate an invalid feed error
114 return;
115 }
116
117 // Before we try to download the feed, make sure we aren't already processing the feed
118 // by looking up the url in our feed cache
119 if (FeedCache.getFeed(this.url))
120 {
121 if (this.downloadCallback)
122 this.downloadCallback.downloaded(this, kNewsBlogFeedIsBusy);
123 return ; // don't do anything, the feed is already in use
124 }
125
126 this.request = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
127 .createInstance(Components.interfaces.nsIXMLHttpRequest);
128 this.request.onprogress = this.onProgress; // must be set before calling .open
129 this.request.open("GET", this.url, true);
130
131 var lastModified = this.lastModified;
132 if (lastModified)
133 this.request.setRequestHeader("If-Modified-Since", lastModified);
134
135 this.request.overrideMimeType("text/xml");
136 this.request.onload = this.onDownloaded;
137 this.request.onerror = this.onDownloadError;
138 FeedCache.putFeed(this);
139 this.request.send(null);
140 },
141
onDownloaded
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
142 onDownloaded: function(aEvent)
143 {
144 var request = aEvent.target;
145 var url = request.channel.originalURI.spec;
146 debug(url + " downloaded");
147 var feed = FeedCache.getFeed(url);
148 if (!feed)
149 throw("error after downloading " + url + ": couldn't retrieve feed from request");
150
151 // if the request has a Last-Modified header on it, then go ahead and remember
152 // that as a property on the feed so we can use it when making future requests.
153 var lastModifiedHeader = request.getResponseHeader('Last-Modified');
154 if (lastModifiedHeader)
155 this.lastModified = lastModifiedHeader;
156
157 feed.parse(); // parse will asynchronously call the download callback when it is done
158 },
159
onProgress
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
160 onProgress: function(aEvent)
161 {
162 var request = aEvent.target;
163 var url = request.channel.originalURI.spec;
164 var feed = FeedCache.getFeed(url);
165
166 if (feed.downloadCallback)
167 feed.downloadCallback.onProgress(feed, aEvent.position, aEvent.totalSize);
168 },
169
onDownloadError
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
170 onDownloadError: function(aEvent)
171 {
172 var request = aEvent.target;
173 var url = request.channel.originalURI.spec;
174 var feed = FeedCache.getFeed(url);
175 if (feed.downloadCallback)
176 {
177 // if the http status code is a 304, then the feed has not been modified since we last downloaded it.
178 var error = kNewsBlogRequestFailure;
179 try
180 {
181 if (request.status == 304)
182 error = kNewsBlogNoNewItems;
183 } catch (ex) {}
184 feed.downloadCallback.downloaded(feed, error);
185 }
186
187 FeedCache.removeFeed(url);
188 },
189
onParseError
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
190 onParseError: function(aFeed)
191 {
192 if (aFeed)
193 {
194 var error;
195
196 if (aFeed.request && aFeed.request.status == 304)
197 error = kNewsBlogNoNewItems;
198 else {
199 error = kNewsBlogInvalidFeed;
200 aFeed.mInvalidFeed = true;
201 }
202
203 if (aFeed.downloadCallback)
204 aFeed.downloadCallback.downloaded(aFeed, error);
205
206 FeedCache.removeFeed(aFeed.url);
207 }
208 },
209
get_url
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
210 get url()
211 {
212 var ds = getSubscriptionsDS(this.server);
213 var url = ds.GetTarget(this.resource, DC_IDENTIFIER, true);
214 if (url)
215 url = url.QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
216 else
217 url = this.resource.Value;
218 return url;
219 },
220
get_title
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
221 get title()
222 {
223 var ds = getSubscriptionsDS(this.server);
224 var title = ds.GetTarget(this.resource, DC_TITLE, true);
225 if (title)
226 title = title.QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
227 return title;
228 },
229
set_title
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
230 set title (aNewTitle)
231 {
232 if (!aNewTitle)
233 return;
234
235 var ds = getSubscriptionsDS(this.server);
236 aNewTitle = rdf.GetLiteral(aNewTitle);
237 var old_title = ds.GetTarget(this.resource, DC_TITLE, true);
238 if (old_title)
239 ds.Change(this.resource, DC_TITLE, old_title, aNewTitle);
240 else
241 ds.Assert(this.resource, DC_TITLE, aNewTitle, true);
242 },
243
get_lastModified
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
244 get lastModified()
245 {
246 var ds = getSubscriptionsDS(this.server);
247 var lastModified = ds.GetTarget(this.resource, DC_LASTMODIFIED, true);
248 if (lastModified)
249 lastModified = lastModified.QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
250 return lastModified;
251 },
252
set_lastModified
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
253 set lastModified(aLastModified)
254 {
255 var ds = getSubscriptionsDS(this.server);
256 aLastModified = rdf.GetLiteral(aLastModified);
257 var old_lastmodified = ds.GetTarget(this.resource, DC_LASTMODIFIED, true);
258 if (old_lastmodified)
259 ds.Change(this.resource, DC_LASTMODIFIED, old_lastmodified, aLastModified);
260 else
261 ds.Assert(this.resource, DC_LASTMODIFIED, aLastModified, true);
262
263 // do we need to flush every time this property changes?
264 ds = ds.QueryInterface(Components.interfaces.nsIRDFRemoteDataSource);
265 ds.Flush();
266 },
267
get_quickMode
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
268 get quickMode ()
269 {
270 var ds = getSubscriptionsDS(this.server);
271 var quickMode = ds.GetTarget(this.resource, FZ_QUICKMODE, true);
272 if (quickMode)
273 {
274 quickMode = quickMode.QueryInterface(Components.interfaces.nsIRDFLiteral);
275 quickMode = quickMode.Value;
276 quickMode = eval(quickMode);
277 }
278 return quickMode;
279 },
280
set_quickMode
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
281 set quickMode (aNewQuickMode)
282 {
283 var ds = getSubscriptionsDS(this.server);
284 aNewQuickMode = rdf.GetLiteral(aNewQuickMode);
285 var old_quickMode = ds.GetTarget(this.resource, FZ_QUICKMODE, true);
286 if (old_quickMode)
287 ds.Change(this.resource, FZ_QUICKMODE, old_quickMode, aNewQuickMode);
288 else
289 ds.Assert(this.resource, FZ_QUICKMODE, aNewQuickMode, true);
290 },
291
get_link
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
292 get link ()
293 {
294 var ds = getSubscriptionsDS(this.server);
295 var link = ds.GetTarget(this.resource, RSS_LINK, true);
296 if(link)
297 link = link.QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
298 return link;
299 },
300
set_link
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
301 set link (aNewLink)
302 {
303 if (!aNewLink)
304 return;
305
306 var ds = getSubscriptionsDS(this.server);
307 aNewLink = rdf.GetLiteral(aNewLink);
308 var old_link = ds.GetTarget(this.resource, RSS_LINK, true);
309 if (old_link)
310 ds.Change(this.resource, RSS_LINK, old_link, aNewLink);
311 else
312 ds.Assert(this.resource, RSS_LINK, aNewLink, true);
313 },
314
parse
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
315 parse: function()
316 {
317 // Figures out what description language (RSS, Atom) and version this feed
318 // is using and calls a language/version-specific feed parser.
319
320 debug("parsing feed " + this.url);
321
322 if (!this.request.responseText)
323 {
324 this.onParseError(this);
325 return;
326 }
327
328 // create a feed parser which will parse the feed for us
329 var parser = new FeedParser();
330 this.itemsToStore = parser.parseFeed(this, this.request.responseText, this.request.responseXML, this.request.channel.URI);
331
332 if (this.mInvalidFeed)
333 {
334 this.request = null;
335 this.mInvalidFeed = false;
336 return;
337 }
338
339 // storeNextItem will iterate through the parsed items, storing each one.
340 this.itemsToStoreIndex = 0;
341 this.storeNextItem();
342 },
343
invalidateItems
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
344 invalidateItems: function ()
345 {
346 var ds = getItemsDS(this.server);
347 debug("invalidating items for " + this.url);
348 var items = ds.GetSources(FZ_FEED, this.resource, true);
349 var item;
350
351 while (items.hasMoreElements())
352 {
353 item = items.getNext();
354 item = item.QueryInterface(Components.interfaces.nsIRDFResource);
355 debug("invalidating " + item.Value);
356 var valid = ds.GetTarget(item, FZ_VALID, true);
357 if (valid)
358 ds.Unassert(item, FZ_VALID, valid, true);
359 }
360 },
361
removeInvalidItems
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
362 removeInvalidItems: function()
363 {
364 var ds = getItemsDS(this.server);
365 debug("removing invalid items for " + this.url);
366 var items = ds.GetSources(FZ_FEED, this.resource, true);
367 var item;
368 while (items.hasMoreElements())
369 {
370 item = items.getNext();
371 item = item.QueryInterface(Components.interfaces.nsIRDFResource);
372 if (ds.HasAssertion(item, FZ_VALID, RDF_LITERAL_TRUE, true))
373 continue;
374 debug("removing " + item.Value);
375 ds.Unassert(item, FZ_FEED, this.resource, true);
376 if (ds.hasArcOut(item, FZ_FEED))
377 debug(item.Value + " is from more than one feed; only the reference to this feed removed");
378 else
379 removeAssertions(ds, item);
380 }
381 },
382
createFolder
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
383 createFolder: function()
384 {
385 if (!this.folder)
386 this.server.rootMsgFolder.createSubfolder(this.name, null /* supposed to be a msg window */);
387 },
388
389 // gets the next item from gItemsToStore and forces that item to be stored
390 // to the folder. If more items are left to be stored, fires a timer for the next one.
391 // otherwise it triggers a download done notification to the UI
storeNextItem
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
392 storeNextItem: function()
393 {
394 if (!this.itemsToStore || !this.itemsToStore.length)
395 {
396 this.createFolder();
397 this.cleanupParsingState(this);
398 return;
399 }
400
401 var item = this.itemsToStore[this.itemsToStoreIndex];
402
403 item.store();
404 item.markValid();
405
406 // if the listener is tracking progress for storing each item, report it here...
407 if (item.feed.downloadCallback && item.feed.downloadCallback.onFeedItemStored)
408 item.feed.downloadCallback.onFeedItemStored(item.feed, this.itemsToStoreIndex, this.itemsToStore.length);
409
410 this.itemsToStoreIndex++
411
412 // eventually we'll report individual progress here....
413
414 if (this.itemsToStoreIndex < this.itemsToStore.length)
415 {
416 if (!this.storeItemsTimer)
417 this.storeItemsTimer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
418 this.storeItemsTimer.initWithCallback(this, 50, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
419 }
420 else
421 {
422 // we have just finished downloading one or more feed items into the destination folder,
423 // if the folder is still listed as having new messages in it, then we should set the biff state on the folder
424 // so the right RDF UI changes happen in the folder pane to indicate new mail.
425
426 if (item.feed.folder.hasNewMessages)
427 item.feed.folder.biffState = Components.interfaces.nsIMsgFolder.nsMsgBiffState_NewMail;
428 this.cleanupParsingState(item.feed);
429 }
430 },
431
cleanupParsingState
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
432 cleanupParsingState: function(aFeed)
433 {
434 // now that we are done parsing the feed, remove the feed from our feed cache
435 FeedCache.removeFeed(aFeed.url);
436 aFeed.removeInvalidItems();
437
438 // let's be sure to flush any feed item changes back to disk
439 var ds = getItemsDS(aFeed.server);
440 ds.QueryInterface(Components.interfaces.nsIRDFRemoteDataSource).Flush(); // flush any changes
441
442 if (aFeed.downloadCallback)
443 aFeed.downloadCallback.downloaded(aFeed, kNewsBlogSuccess);
444
445 this.request = null; // force the xml http request to go away. This helps reduce some nasty assertions on shut down.
446 this.itemsToStore = "";
447 this.itemsToStoreIndex = 0;
448 this.storeItemsTimer = null;
449 },
450
notify
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
451 notify: function(aTimer)
452 {
453 this.storeNextItem();
454 },
455
QueryInterface
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
456 QueryInterface: function(aIID)
457 {
458 if (aIID.equals(Components.interfaces.nsITimerCallback) || aIID.equals(Components.interfaces.nsISupports))
459 return this;
460
461 Components.returnCode = Components.results.NS_ERROR_NO_INTERFACE;
462 return null;
463 }
464 };