|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 const Cc = Components.classes; |
|
6 const Ci = Components.interfaces; |
|
7 const Cr = Components.results; |
|
8 const Cu = Components.utils; |
|
9 |
|
10 //////////////////////////////////////////////////////////////////////////////// |
|
11 //// Modules |
|
12 |
|
13 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
14 Cu.import("resource://gre/modules/Services.jsm"); |
|
15 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", |
|
16 "resource://gre/modules/PlacesUtils.jsm"); |
|
17 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", |
|
18 "resource://gre/modules/NetUtil.jsm"); |
|
19 XPCOMUtils.defineLazyModuleGetter(this, "Promise", |
|
20 "resource://gre/modules/Promise.jsm"); |
|
21 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated", |
|
22 "resource://gre/modules/Deprecated.jsm"); |
|
23 |
|
24 //////////////////////////////////////////////////////////////////////////////// |
|
25 //// Services |
|
26 |
|
27 XPCOMUtils.defineLazyServiceGetter(this, "secMan", |
|
28 "@mozilla.org/scriptsecuritymanager;1", |
|
29 "nsIScriptSecurityManager"); |
|
30 XPCOMUtils.defineLazyGetter(this, "asyncHistory", function () { |
|
31 // Lazily add an history observer when it's actually needed. |
|
32 PlacesUtils.history.addObserver(PlacesUtils.livemarks, true); |
|
33 return Cc["@mozilla.org/browser/history;1"].getService(Ci.mozIAsyncHistory); |
|
34 }); |
|
35 |
|
36 //////////////////////////////////////////////////////////////////////////////// |
|
37 //// Constants |
|
38 |
|
39 // Security flags for checkLoadURIWithPrincipal. |
|
40 const SEC_FLAGS = Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL; |
|
41 |
|
42 // Delay between reloads of consecute livemarks. |
|
43 const RELOAD_DELAY_MS = 500; |
|
44 // Expire livemarks after this time. |
|
45 const EXPIRE_TIME_MS = 3600000; // 1 hour. |
|
46 // Expire livemarks after this time on error. |
|
47 const ONERROR_EXPIRE_TIME_MS = 300000; // 5 minutes. |
|
48 |
|
49 //////////////////////////////////////////////////////////////////////////////// |
|
50 //// LivemarkService |
|
51 |
|
52 function LivemarkService() |
|
53 { |
|
54 // Cleanup on shutdown. |
|
55 Services.obs.addObserver(this, PlacesUtils.TOPIC_SHUTDOWN, true); |
|
56 |
|
57 // Observe bookmarks and history, but don't init the services just for that. |
|
58 PlacesUtils.addLazyBookmarkObserver(this, true); |
|
59 |
|
60 // Asynchronously build the livemarks cache. |
|
61 this._ensureAsynchronousCache(); |
|
62 } |
|
63 |
|
64 LivemarkService.prototype = { |
|
65 // Cache of Livemark objects, hashed by bookmarks folder ids. |
|
66 _livemarks: {}, |
|
67 // Hash associating guids to bookmarks folder ids. |
|
68 _guids: {}, |
|
69 |
|
70 get _populateCacheSQL() |
|
71 { |
|
72 function getAnnoSQLFragment(aAnnoParam) { |
|
73 return "SELECT a.content " |
|
74 + "FROM moz_items_annos a " |
|
75 + "JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id " |
|
76 + "WHERE a.item_id = b.id " |
|
77 + "AND n.name = " + aAnnoParam; |
|
78 } |
|
79 |
|
80 return "SELECT b.id, b.title, b.parent, b.position, b.guid, b.lastModified, " |
|
81 + "(" + getAnnoSQLFragment(":feedURI_anno") + ") AS feedURI, " |
|
82 + "(" + getAnnoSQLFragment(":siteURI_anno") + ") AS siteURI " |
|
83 + "FROM moz_bookmarks b " |
|
84 + "JOIN moz_items_annos a ON a.item_id = b.id " |
|
85 + "JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id " |
|
86 + "WHERE b.type = :folder_type " |
|
87 + "AND n.name = :feedURI_anno "; |
|
88 }, |
|
89 |
|
90 _ensureAsynchronousCache: function LS__ensureAsynchronousCache() |
|
91 { |
|
92 let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) |
|
93 .DBConnection; |
|
94 let stmt = db.createAsyncStatement(this._populateCacheSQL); |
|
95 stmt.params.folder_type = Ci.nsINavBookmarksService.TYPE_FOLDER; |
|
96 stmt.params.feedURI_anno = PlacesUtils.LMANNO_FEEDURI; |
|
97 stmt.params.siteURI_anno = PlacesUtils.LMANNO_SITEURI; |
|
98 |
|
99 let livemarkSvc = this; |
|
100 this._pendingStmt = stmt.executeAsync({ |
|
101 handleResult: function LS_handleResult(aResults) |
|
102 { |
|
103 for (let row = aResults.getNextRow(); row; row = aResults.getNextRow()) { |
|
104 let id = row.getResultByName("id"); |
|
105 let siteURL = row.getResultByName("siteURI"); |
|
106 let guid = row.getResultByName("guid"); |
|
107 livemarkSvc._livemarks[id] = |
|
108 new Livemark({ id: id, |
|
109 guid: guid, |
|
110 title: row.getResultByName("title"), |
|
111 parentId: row.getResultByName("parent"), |
|
112 index: row.getResultByName("position"), |
|
113 lastModified: row.getResultByName("lastModified"), |
|
114 feedURI: NetUtil.newURI(row.getResultByName("feedURI")), |
|
115 siteURI: siteURL ? NetUtil.newURI(siteURL) : null, |
|
116 }); |
|
117 livemarkSvc._guids[guid] = id; |
|
118 } |
|
119 }, |
|
120 handleError: function LS_handleError(aErr) |
|
121 { |
|
122 Cu.reportError("AsyncStmt error (" + aErr.result + "): '" + aErr.message); |
|
123 }, |
|
124 handleCompletion: function LS_handleCompletion() { |
|
125 livemarkSvc._pendingStmt = null; |
|
126 } |
|
127 }); |
|
128 stmt.finalize(); |
|
129 }, |
|
130 |
|
131 _onCacheReady: function LS__onCacheReady(aCallback) |
|
132 { |
|
133 if (this._pendingStmt) { |
|
134 // The cache is still being populated, so enqueue the job to the Storage |
|
135 // async thread. Ideally this should just dispatch a runnable to it, |
|
136 // that would call back on the main thread, but bug 608142 made that |
|
137 // impossible. Thus just enqueue the cheapest query possible. |
|
138 let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) |
|
139 .DBConnection; |
|
140 let stmt = db.createAsyncStatement("PRAGMA encoding"); |
|
141 stmt.executeAsync({ |
|
142 handleError: function () {}, |
|
143 handleResult: function () {}, |
|
144 handleCompletion: function ETAT_handleCompletion() |
|
145 { |
|
146 aCallback(); |
|
147 } |
|
148 }); |
|
149 stmt.finalize(); |
|
150 } |
|
151 else { |
|
152 // The callbacks should always be enqueued per the interface. |
|
153 // Just enque on the main thread. |
|
154 Services.tm.mainThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL); |
|
155 } |
|
156 }, |
|
157 |
|
158 _reloading: false, |
|
159 _startReloadTimer: function LS__startReloadTimer() |
|
160 { |
|
161 if (this._reloadTimer) { |
|
162 this._reloadTimer.cancel(); |
|
163 } |
|
164 else { |
|
165 this._reloadTimer = Cc["@mozilla.org/timer;1"] |
|
166 .createInstance(Ci.nsITimer); |
|
167 } |
|
168 this._reloading = true; |
|
169 this._reloadTimer.initWithCallback(this._reloadNextLivemark.bind(this), |
|
170 RELOAD_DELAY_MS, |
|
171 Ci.nsITimer.TYPE_ONE_SHOT); |
|
172 }, |
|
173 |
|
174 ////////////////////////////////////////////////////////////////////////////// |
|
175 //// nsIObserver |
|
176 |
|
177 observe: function LS_observe(aSubject, aTopic, aData) |
|
178 { |
|
179 if (aTopic == PlacesUtils.TOPIC_SHUTDOWN) { |
|
180 if (this._pendingStmt) { |
|
181 this._pendingStmt.cancel(); |
|
182 this._pendingStmt = null; |
|
183 // Initialization never finished, so just bail out. |
|
184 return; |
|
185 } |
|
186 |
|
187 if (this._reloadTimer) { |
|
188 this._reloading = false; |
|
189 this._reloadTimer.cancel(); |
|
190 delete this._reloadTimer; |
|
191 } |
|
192 |
|
193 // Stop any ongoing update. |
|
194 for each (let livemark in this._livemarks) { |
|
195 livemark.terminate(); |
|
196 } |
|
197 this._livemarks = {}; |
|
198 } |
|
199 }, |
|
200 |
|
201 ////////////////////////////////////////////////////////////////////////////// |
|
202 //// mozIAsyncLivemarks |
|
203 |
|
204 addLivemark: function LS_addLivemark(aLivemarkInfo, |
|
205 aLivemarkCallback) |
|
206 { |
|
207 // Must provide at least non-null parentId, index and feedURI. |
|
208 if (!aLivemarkInfo || |
|
209 ("parentId" in aLivemarkInfo && aLivemarkInfo.parentId < 1) || |
|
210 !("index" in aLivemarkInfo) || aLivemarkInfo.index < Ci.nsINavBookmarksService.DEFAULT_INDEX || |
|
211 !(aLivemarkInfo.feedURI instanceof Ci.nsIURI) || |
|
212 (aLivemarkInfo.siteURI && !(aLivemarkInfo.siteURI instanceof Ci.nsIURI)) || |
|
213 (aLivemarkInfo.guid && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid))) { |
|
214 throw Cr.NS_ERROR_INVALID_ARG; |
|
215 } |
|
216 |
|
217 if (aLivemarkCallback) { |
|
218 Deprecated.warning("Passing a callback to Livermarks methods is deprecated. " + |
|
219 "Please use the returned promise instead.", |
|
220 "https://developer.mozilla.org/docs/Mozilla/JavaScript_code_modules/Promise.jsm"); |
|
221 } |
|
222 |
|
223 // The addition is done synchronously due to the fact importExport service |
|
224 // and JSON backups require that. The notification is async though. |
|
225 // Once bookmarks are async, this may be properly fixed. |
|
226 let deferred = Promise.defer(); |
|
227 let addLivemarkEx = null; |
|
228 let livemark = null; |
|
229 try { |
|
230 // Disallow adding a livemark inside another livemark. |
|
231 if (aLivemarkInfo.parentId in this._livemarks) { |
|
232 throw new Components.Exception("", Cr.NS_ERROR_INVALID_ARG); |
|
233 } |
|
234 |
|
235 // Don't pass unexpected input data to the livemark constructor. |
|
236 livemark = new Livemark({ title: aLivemarkInfo.title |
|
237 , parentId: aLivemarkInfo.parentId |
|
238 , index: aLivemarkInfo.index |
|
239 , feedURI: aLivemarkInfo.feedURI |
|
240 , siteURI: aLivemarkInfo.siteURI |
|
241 , guid: aLivemarkInfo.guid |
|
242 , lastModified: aLivemarkInfo.lastModified |
|
243 }); |
|
244 if (this._itemAdded && this._itemAdded.id == livemark.id) { |
|
245 livemark.index = this._itemAdded.index; |
|
246 livemark.guid = this._itemAdded.guid; |
|
247 if (!aLivemarkInfo.lastModified) { |
|
248 livemark.lastModified = this._itemAdded.lastModified; |
|
249 } |
|
250 } |
|
251 |
|
252 // Updating the cache even if it has not yet been populated doesn't |
|
253 // matter since it will just be overwritten. |
|
254 this._livemarks[livemark.id] = livemark; |
|
255 this._guids[livemark.guid] = livemark.id; |
|
256 } |
|
257 catch (ex) { |
|
258 addLivemarkEx = ex; |
|
259 livemark = null; |
|
260 } |
|
261 finally { |
|
262 this._onCacheReady( () => { |
|
263 if (addLivemarkEx) { |
|
264 if (aLivemarkCallback) { |
|
265 try { |
|
266 aLivemarkCallback.onCompletion(addLivemarkEx.result, livemark); |
|
267 } |
|
268 catch(ex2) { } |
|
269 } else { |
|
270 deferred.reject(addLivemarkEx); |
|
271 } |
|
272 } |
|
273 else { |
|
274 if (aLivemarkCallback) { |
|
275 try { |
|
276 aLivemarkCallback.onCompletion(Cr.NS_OK, livemark); |
|
277 } |
|
278 catch(ex2) { } |
|
279 } else { |
|
280 deferred.resolve(livemark); |
|
281 } |
|
282 } |
|
283 }); |
|
284 } |
|
285 |
|
286 return aLivemarkCallback ? null : deferred.promise; |
|
287 }, |
|
288 |
|
289 removeLivemark: function LS_removeLivemark(aLivemarkInfo, aLivemarkCallback) |
|
290 { |
|
291 if (!aLivemarkInfo) { |
|
292 throw Cr.NS_ERROR_INVALID_ARG; |
|
293 } |
|
294 |
|
295 // Accept either a guid or an id. |
|
296 let id = aLivemarkInfo.guid || aLivemarkInfo.id; |
|
297 if (("guid" in aLivemarkInfo && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) || |
|
298 ("id" in aLivemarkInfo && aLivemarkInfo.id < 1) || |
|
299 !id) { |
|
300 throw Cr.NS_ERROR_INVALID_ARG; |
|
301 } |
|
302 |
|
303 if (aLivemarkCallback) { |
|
304 Deprecated.warning("Passing a callback to Livermarks methods is deprecated. " + |
|
305 "Please use the returned promise instead.", |
|
306 "https://developer.mozilla.org/docs/Mozilla/JavaScript_code_modules/Promise.jsm"); |
|
307 } |
|
308 |
|
309 // Convert the guid to an id. |
|
310 if (id in this._guids) { |
|
311 id = this._guids[id]; |
|
312 } |
|
313 |
|
314 let deferred = Promise.defer(); |
|
315 let removeLivemarkEx = null; |
|
316 try { |
|
317 if (!(id in this._livemarks)) { |
|
318 throw new Components.Exception("", Cr.NS_ERROR_INVALID_ARG); |
|
319 } |
|
320 this._livemarks[id].remove(); |
|
321 } |
|
322 catch (ex) { |
|
323 removeLivemarkEx = ex; |
|
324 } |
|
325 finally { |
|
326 this._onCacheReady( () => { |
|
327 if (removeLivemarkEx) { |
|
328 if (aLivemarkCallback) { |
|
329 try { |
|
330 aLivemarkCallback.onCompletion(removeLivemarkEx.result, null); |
|
331 } |
|
332 catch(ex2) { } |
|
333 } else { |
|
334 deferred.reject(removeLivemarkEx); |
|
335 } |
|
336 } |
|
337 else { |
|
338 if (aLivemarkCallback) { |
|
339 try { |
|
340 aLivemarkCallback.onCompletion(Cr.NS_OK, null); |
|
341 } |
|
342 catch(ex2) { } |
|
343 } else { |
|
344 deferred.resolve(); |
|
345 } |
|
346 } |
|
347 }); |
|
348 } |
|
349 |
|
350 return aLivemarkCallback ? null : deferred.promise; |
|
351 }, |
|
352 |
|
353 _reloaded: [], |
|
354 _reloadNextLivemark: function LS__reloadNextLivemark() |
|
355 { |
|
356 this._reloading = false; |
|
357 // Find first livemark to be reloaded. |
|
358 for (let id in this._livemarks) { |
|
359 if (this._reloaded.indexOf(id) == -1) { |
|
360 this._reloaded.push(id); |
|
361 this._livemarks[id].reload(this._forceUpdate); |
|
362 this._startReloadTimer(); |
|
363 break; |
|
364 } |
|
365 } |
|
366 }, |
|
367 |
|
368 reloadLivemarks: function LS_reloadLivemarks(aForceUpdate) |
|
369 { |
|
370 // Check if there's a currently running reload, to save some useless work. |
|
371 let notWorthRestarting = |
|
372 this._forceUpdate || // We're already forceUpdating. |
|
373 !aForceUpdate; // The caller didn't request a forced update. |
|
374 if (this._reloading && notWorthRestarting) { |
|
375 // Ignore this call. |
|
376 return; |
|
377 } |
|
378 |
|
379 this._onCacheReady( () => { |
|
380 this._forceUpdate = !!aForceUpdate; |
|
381 this._reloaded = []; |
|
382 // Livemarks reloads happen on a timer, and are delayed for performance |
|
383 // reasons. |
|
384 this._startReloadTimer(); |
|
385 }); |
|
386 }, |
|
387 |
|
388 getLivemark: function LS_getLivemark(aLivemarkInfo, aLivemarkCallback) |
|
389 { |
|
390 if (!aLivemarkInfo) { |
|
391 throw Cr.NS_ERROR_INVALID_ARG; |
|
392 } |
|
393 // Accept either a guid or an id. |
|
394 let id = aLivemarkInfo.guid || aLivemarkInfo.id; |
|
395 if (("guid" in aLivemarkInfo && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) || |
|
396 ("id" in aLivemarkInfo && aLivemarkInfo.id < 1) || |
|
397 !id) { |
|
398 throw Cr.NS_ERROR_INVALID_ARG; |
|
399 } |
|
400 |
|
401 if (aLivemarkCallback) { |
|
402 Deprecated.warning("Passing a callback to Livermarks methods is deprecated. " + |
|
403 "Please use the returned promise instead.", |
|
404 "https://developer.mozilla.org/docs/Mozilla/JavaScript_code_modules/Promise.jsm"); |
|
405 } |
|
406 |
|
407 let deferred = Promise.defer(); |
|
408 this._onCacheReady( () => { |
|
409 // Convert the guid to an id. |
|
410 if (id in this._guids) { |
|
411 id = this._guids[id]; |
|
412 } |
|
413 if (id in this._livemarks) { |
|
414 if (aLivemarkCallback) { |
|
415 try { |
|
416 aLivemarkCallback.onCompletion(Cr.NS_OK, this._livemarks[id]); |
|
417 } catch (ex) {} |
|
418 } else { |
|
419 deferred.resolve(this._livemarks[id]); |
|
420 } |
|
421 } |
|
422 else { |
|
423 if (aLivemarkCallback) { |
|
424 try { |
|
425 aLivemarkCallback.onCompletion(Cr.NS_ERROR_INVALID_ARG, null); |
|
426 } catch (ex) { } |
|
427 } else { |
|
428 deferred.reject(Components.Exception("", Cr.NS_ERROR_INVALID_ARG)); |
|
429 } |
|
430 } |
|
431 }); |
|
432 |
|
433 return aLivemarkCallback ? null : deferred.promise; |
|
434 }, |
|
435 |
|
436 ////////////////////////////////////////////////////////////////////////////// |
|
437 //// nsINavBookmarkObserver |
|
438 |
|
439 onBeginUpdateBatch: function () {}, |
|
440 onEndUpdateBatch: function () {}, |
|
441 onItemVisited: function () {}, |
|
442 |
|
443 _itemAdded: null, |
|
444 onItemAdded: function LS_onItemAdded(aItemId, aParentId, aIndex, aItemType, |
|
445 aURI, aTitle, aDateAdded, aGUID) |
|
446 { |
|
447 if (aItemType == Ci.nsINavBookmarksService.TYPE_FOLDER) { |
|
448 this._itemAdded = { id: aItemId |
|
449 , guid: aGUID |
|
450 , index: aIndex |
|
451 , lastModified: aDateAdded |
|
452 }; |
|
453 } |
|
454 }, |
|
455 |
|
456 onItemChanged: function LS_onItemChanged(aItemId, aProperty, aIsAnno, aValue, |
|
457 aLastModified, aItemType) |
|
458 { |
|
459 if (aItemType == Ci.nsINavBookmarksService.TYPE_FOLDER) { |
|
460 if (this._itemAdded && this._itemAdded.id == aItemId) { |
|
461 this._itemAdded.lastModified = aLastModified; |
|
462 } |
|
463 if (aItemId in this._livemarks) { |
|
464 if (aProperty == "title") { |
|
465 this._livemarks[aItemId].title = aValue; |
|
466 } |
|
467 this._livemarks[aItemId].lastModified = aLastModified; |
|
468 } |
|
469 } |
|
470 }, |
|
471 |
|
472 onItemMoved: function LS_onItemMoved(aItemId, aOldParentId, aOldIndex, |
|
473 aNewParentId, aNewIndex, aItemType) |
|
474 { |
|
475 if (aItemType == Ci.nsINavBookmarksService.TYPE_FOLDER && |
|
476 aItemId in this._livemarks) { |
|
477 this._livemarks[aItemId].parentId = aNewParentId; |
|
478 this._livemarks[aItemId].index = aNewIndex; |
|
479 } |
|
480 }, |
|
481 |
|
482 onItemRemoved: function LS_onItemRemoved(aItemId, aParentId, aIndex, |
|
483 aItemType, aURI, aGUID) |
|
484 { |
|
485 if (aItemType == Ci.nsINavBookmarksService.TYPE_FOLDER && |
|
486 aItemId in this._livemarks) { |
|
487 this._livemarks[aItemId].terminate(); |
|
488 delete this._livemarks[aItemId]; |
|
489 delete this._guids[aGUID]; |
|
490 } |
|
491 }, |
|
492 |
|
493 ////////////////////////////////////////////////////////////////////////////// |
|
494 //// nsINavHistoryObserver |
|
495 |
|
496 onBeginUpdateBatch: function () {}, |
|
497 onEndUpdateBatch: function () {}, |
|
498 onPageChanged: function () {}, |
|
499 onTitleChanged: function () {}, |
|
500 onDeleteVisits: function () {}, |
|
501 onClearHistory: function () {}, |
|
502 |
|
503 onDeleteURI: function PS_onDeleteURI(aURI) { |
|
504 for each (let livemark in this._livemarks) { |
|
505 livemark.updateURIVisitedStatus(aURI, false); |
|
506 } |
|
507 }, |
|
508 |
|
509 onVisit: function PS_onVisit(aURI) { |
|
510 for each (let livemark in this._livemarks) { |
|
511 livemark.updateURIVisitedStatus(aURI, true); |
|
512 } |
|
513 }, |
|
514 |
|
515 ////////////////////////////////////////////////////////////////////////////// |
|
516 //// nsISupports |
|
517 |
|
518 classID: Components.ID("{dca61eb5-c7cd-4df1-b0fb-d0722baba251}"), |
|
519 |
|
520 _xpcom_factory: XPCOMUtils.generateSingletonFactory(LivemarkService), |
|
521 |
|
522 QueryInterface: XPCOMUtils.generateQI([ |
|
523 Ci.mozIAsyncLivemarks |
|
524 , Ci.nsINavBookmarkObserver |
|
525 , Ci.nsINavHistoryObserver |
|
526 , Ci.nsIObserver |
|
527 , Ci.nsISupportsWeakReference |
|
528 ]) |
|
529 }; |
|
530 |
|
531 //////////////////////////////////////////////////////////////////////////////// |
|
532 //// Livemark |
|
533 |
|
534 /** |
|
535 * Object used internally to represent a livemark. |
|
536 * |
|
537 * @param aLivemarkInfo |
|
538 * Object containing information on the livemark. If the livemark is |
|
539 * not included in the object, a new livemark will be created. |
|
540 * |
|
541 * @note terminate() must be invoked before getting rid of this object. |
|
542 */ |
|
543 function Livemark(aLivemarkInfo) |
|
544 { |
|
545 this.title = aLivemarkInfo.title; |
|
546 this.parentId = aLivemarkInfo.parentId; |
|
547 this.index = aLivemarkInfo.index; |
|
548 |
|
549 this._status = Ci.mozILivemark.STATUS_READY; |
|
550 |
|
551 // Hash of resultObservers, hashed by container. |
|
552 this._resultObservers = new Map(); |
|
553 // This keeps a list of the containers used as keys in the map, since |
|
554 // it's not iterable. In future may use an iterable Map. |
|
555 this._resultObserversList = []; |
|
556 |
|
557 // Sorted array of objects representing livemark children in the form |
|
558 // { uri, title, visited }. |
|
559 this._children = []; |
|
560 |
|
561 // Keeps a separate array of nodes for each requesting container, hashed by |
|
562 // the container itself. |
|
563 this._nodes = new Map(); |
|
564 |
|
565 this._guid = ""; |
|
566 this._lastModified = 0; |
|
567 |
|
568 this.loadGroup = null; |
|
569 this.feedURI = null; |
|
570 this.siteURI = null; |
|
571 this.expireTime = 0; |
|
572 |
|
573 if (aLivemarkInfo.id) { |
|
574 // This request comes from the cache. |
|
575 this.id = aLivemarkInfo.id; |
|
576 this.guid = aLivemarkInfo.guid; |
|
577 this.feedURI = aLivemarkInfo.feedURI; |
|
578 this.siteURI = aLivemarkInfo.siteURI; |
|
579 this.lastModified = aLivemarkInfo.lastModified; |
|
580 } |
|
581 else { |
|
582 // Create a new livemark. |
|
583 this.id = PlacesUtils.bookmarks.createFolder(aLivemarkInfo.parentId, |
|
584 aLivemarkInfo.title, |
|
585 aLivemarkInfo.index, |
|
586 aLivemarkInfo.guid); |
|
587 PlacesUtils.bookmarks.setFolderReadonly(this.id, true); |
|
588 this.writeFeedURI(aLivemarkInfo.feedURI); |
|
589 if (aLivemarkInfo.siteURI) { |
|
590 this.writeSiteURI(aLivemarkInfo.siteURI); |
|
591 } |
|
592 // Last modified time must be the last change. |
|
593 if (aLivemarkInfo.lastModified) { |
|
594 this.lastModified = aLivemarkInfo.lastModified; |
|
595 PlacesUtils.bookmarks.setItemLastModified(this.id, this.lastModified); |
|
596 } |
|
597 } |
|
598 } |
|
599 |
|
600 Livemark.prototype = { |
|
601 get status() this._status, |
|
602 set status(val) { |
|
603 if (this._status != val) { |
|
604 this._status = val; |
|
605 this._invalidateRegisteredContainers(); |
|
606 } |
|
607 return this._status; |
|
608 }, |
|
609 |
|
610 /** |
|
611 * Sets an annotation on the bookmarks folder id representing the livemark. |
|
612 * |
|
613 * @param aAnnoName |
|
614 * Name of the annotation. |
|
615 * @param aValue |
|
616 * Value of the annotation. |
|
617 * @return The annotation value. |
|
618 * @throws If the folder is invalid. |
|
619 */ |
|
620 _setAnno: function LM__setAnno(aAnnoName, aValue) |
|
621 { |
|
622 PlacesUtils.annotations |
|
623 .setItemAnnotation(this.id, aAnnoName, aValue, 0, |
|
624 PlacesUtils.annotations.EXPIRE_NEVER); |
|
625 }, |
|
626 |
|
627 writeFeedURI: function LM_writeFeedURI(aFeedURI) |
|
628 { |
|
629 this._setAnno(PlacesUtils.LMANNO_FEEDURI, aFeedURI.spec); |
|
630 this.feedURI = aFeedURI; |
|
631 }, |
|
632 |
|
633 writeSiteURI: function LM_writeSiteURI(aSiteURI) |
|
634 { |
|
635 if (!aSiteURI) { |
|
636 PlacesUtils.annotations.removeItemAnnotation(this.id, |
|
637 PlacesUtils.LMANNO_SITEURI) |
|
638 this.siteURI = null; |
|
639 return; |
|
640 } |
|
641 |
|
642 // Security check the site URI against the feed URI principal. |
|
643 let feedPrincipal = secMan.getSimpleCodebasePrincipal(this.feedURI); |
|
644 try { |
|
645 secMan.checkLoadURIWithPrincipal(feedPrincipal, aSiteURI, SEC_FLAGS); |
|
646 } |
|
647 catch (ex) { |
|
648 return; |
|
649 } |
|
650 |
|
651 this._setAnno(PlacesUtils.LMANNO_SITEURI, aSiteURI.spec) |
|
652 this.siteURI = aSiteURI; |
|
653 }, |
|
654 |
|
655 set guid(aGUID) { |
|
656 this._guid = aGUID; |
|
657 return aGUID; |
|
658 }, |
|
659 get guid() this._guid, |
|
660 |
|
661 set lastModified(aLastModified) { |
|
662 this._lastModified = aLastModified; |
|
663 return aLastModified; |
|
664 }, |
|
665 get lastModified() this._lastModified, |
|
666 |
|
667 /** |
|
668 * Tries to updates the livemark if needed. |
|
669 * The update process is asynchronous. |
|
670 * |
|
671 * @param [optional] aForceUpdate |
|
672 * If true will try to update the livemark even if its contents have |
|
673 * not yet expired. |
|
674 */ |
|
675 updateChildren: function LM_updateChildren(aForceUpdate) |
|
676 { |
|
677 // Check if the livemark is already updating. |
|
678 if (this.status == Ci.mozILivemark.STATUS_LOADING) |
|
679 return; |
|
680 |
|
681 // Check the TTL/expiration on this, to check if there is no need to update |
|
682 // this livemark. |
|
683 if (!aForceUpdate && this.children.length && this.expireTime > Date.now()) |
|
684 return; |
|
685 |
|
686 this.status = Ci.mozILivemark.STATUS_LOADING; |
|
687 |
|
688 // Setting the status notifies observers that may remove the livemark. |
|
689 if (this._terminated) |
|
690 return; |
|
691 |
|
692 try { |
|
693 // Create a load group for the request. This will allow us to |
|
694 // automatically keep track of redirects, so we can always |
|
695 // cancel the channel. |
|
696 let loadgroup = Cc["@mozilla.org/network/load-group;1"]. |
|
697 createInstance(Ci.nsILoadGroup); |
|
698 let channel = NetUtil.newChannel(this.feedURI.spec). |
|
699 QueryInterface(Ci.nsIHttpChannel); |
|
700 channel.loadGroup = loadgroup; |
|
701 channel.loadFlags |= Ci.nsIRequest.LOAD_BACKGROUND | |
|
702 Ci.nsIRequest.LOAD_BYPASS_CACHE; |
|
703 channel.requestMethod = "GET"; |
|
704 channel.setRequestHeader("X-Moz", "livebookmarks", false); |
|
705 |
|
706 // Stream the result to the feed parser with this listener |
|
707 let listener = new LivemarkLoadListener(this); |
|
708 channel.notificationCallbacks = listener; |
|
709 channel.asyncOpen(listener, null); |
|
710 |
|
711 this.loadGroup = loadgroup; |
|
712 } |
|
713 catch (ex) { |
|
714 this.status = Ci.mozILivemark.STATUS_FAILED; |
|
715 } |
|
716 }, |
|
717 |
|
718 reload: function LM_reload(aForceUpdate) |
|
719 { |
|
720 this.updateChildren(aForceUpdate); |
|
721 }, |
|
722 |
|
723 remove: function LM_remove() { |
|
724 PlacesUtils.bookmarks.removeItem(this.id); |
|
725 }, |
|
726 |
|
727 get children() this._children, |
|
728 set children(val) { |
|
729 this._children = val; |
|
730 |
|
731 // Discard the previous cached nodes, new ones should be generated. |
|
732 for (let i = 0; i < this._resultObserversList.length; i++) { |
|
733 let container = this._resultObserversList[i]; |
|
734 this._nodes.delete(container); |
|
735 } |
|
736 |
|
737 // Update visited status for each entry. |
|
738 for (let i = 0; i < this._children.length; i++) { |
|
739 let child = this._children[i]; |
|
740 asyncHistory.isURIVisited(child.uri, |
|
741 (function(aURI, aIsVisited) { |
|
742 this.updateURIVisitedStatus(aURI, aIsVisited); |
|
743 }).bind(this)); |
|
744 } |
|
745 |
|
746 return this._children; |
|
747 }, |
|
748 |
|
749 _isURIVisited: function LM__isURIVisited(aURI) { |
|
750 for (let i = 0; i < this.children.length; i++) { |
|
751 if (this.children[i].uri.equals(aURI)) { |
|
752 return this.children[i].visited; |
|
753 } |
|
754 } |
|
755 }, |
|
756 |
|
757 getNodesForContainer: function LM_getNodesForContainer(aContainerNode) |
|
758 { |
|
759 if (this._nodes.has(aContainerNode)) { |
|
760 return this._nodes.get(aContainerNode); |
|
761 } |
|
762 |
|
763 let livemark = this; |
|
764 let nodes = []; |
|
765 let now = Date.now() * 1000; |
|
766 for (let i = 0; i < this._children.length; i++) { |
|
767 let child = this._children[i]; |
|
768 let node = { |
|
769 // The QueryInterface is needed cause aContainerNode is a jsval. |
|
770 // This is required to avoid issues with scriptable wrappers that would |
|
771 // not allow the view to correctly set expandos. |
|
772 get parent() |
|
773 aContainerNode.QueryInterface(Ci.nsINavHistoryContainerResultNode), |
|
774 get parentResult() this.parent.parentResult, |
|
775 get uri() child.uri.spec, |
|
776 get type() Ci.nsINavHistoryResultNode.RESULT_TYPE_URI, |
|
777 get title() child.title, |
|
778 get accessCount() |
|
779 Number(livemark._isURIVisited(NetUtil.newURI(this.uri))), |
|
780 get time() 0, |
|
781 get icon() "", |
|
782 get indentLevel() this.parent.indentLevel + 1, |
|
783 get bookmarkIndex() -1, |
|
784 get itemId() -1, |
|
785 get dateAdded() now + i, |
|
786 get lastModified() now + i, |
|
787 get tags() |
|
788 PlacesUtils.tagging.getTagsForURI(NetUtil.newURI(this.uri)).join(", "), |
|
789 QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryResultNode]) |
|
790 }; |
|
791 nodes.push(node); |
|
792 } |
|
793 this._nodes.set(aContainerNode, nodes); |
|
794 return nodes; |
|
795 }, |
|
796 |
|
797 registerForUpdates: function LM_registerForUpdates(aContainerNode, |
|
798 aResultObserver) |
|
799 { |
|
800 this._resultObservers.set(aContainerNode, aResultObserver); |
|
801 this._resultObserversList.push(aContainerNode); |
|
802 }, |
|
803 |
|
804 unregisterForUpdates: function LM_unregisterForUpdates(aContainerNode) |
|
805 { |
|
806 this._resultObservers.delete(aContainerNode); |
|
807 let index = this._resultObserversList.indexOf(aContainerNode); |
|
808 this._resultObserversList.splice(index, 1); |
|
809 |
|
810 this._nodes.delete(aContainerNode); |
|
811 }, |
|
812 |
|
813 _invalidateRegisteredContainers: function LM__invalidateRegisteredContainers() |
|
814 { |
|
815 for (let i = 0; i < this._resultObserversList.length; i++) { |
|
816 let container = this._resultObserversList[i]; |
|
817 let observer = this._resultObservers.get(container); |
|
818 observer.invalidateContainer(container); |
|
819 } |
|
820 }, |
|
821 |
|
822 updateURIVisitedStatus: |
|
823 function LM_updateURIVisitedStatus(aURI, aVisitedStatus) |
|
824 { |
|
825 for (let i = 0; i < this.children.length; i++) { |
|
826 if (this.children[i].uri.equals(aURI)) { |
|
827 this.children[i].visited = aVisitedStatus; |
|
828 } |
|
829 } |
|
830 |
|
831 for (let i = 0; i < this._resultObserversList.length; i++) { |
|
832 let container = this._resultObserversList[i]; |
|
833 let observer = this._resultObservers.get(container); |
|
834 if (this._nodes.has(container)) { |
|
835 let nodes = this._nodes.get(container); |
|
836 for (let j = 0; j < nodes.length; j++) { |
|
837 let node = nodes[j]; |
|
838 if (node.uri == aURI.spec) { |
|
839 Services.tm.mainThread.dispatch((function () { |
|
840 observer.nodeHistoryDetailsChanged(node, 0, aVisitedStatus); |
|
841 }).bind(this), Ci.nsIThread.DISPATCH_NORMAL); |
|
842 } |
|
843 } |
|
844 } |
|
845 } |
|
846 }, |
|
847 |
|
848 /** |
|
849 * Terminates the livemark entry, cancelling any ongoing load. |
|
850 * Must be invoked before destroying the entry. |
|
851 */ |
|
852 terminate: function LM_terminate() |
|
853 { |
|
854 // Avoid handling any updateChildren request from now on. |
|
855 this._terminated = true; |
|
856 // Clear the list before aborting, since abort() would try to set the |
|
857 // status and notify about it, but that's not really useful at this point. |
|
858 this._resultObserversList = []; |
|
859 this.abort(); |
|
860 }, |
|
861 |
|
862 /** |
|
863 * Aborts the livemark loading if needed. |
|
864 */ |
|
865 abort: function LM_abort() |
|
866 { |
|
867 this.status = Ci.mozILivemark.STATUS_FAILED; |
|
868 if (this.loadGroup) { |
|
869 this.loadGroup.cancel(Cr.NS_BINDING_ABORTED); |
|
870 this.loadGroup = null; |
|
871 } |
|
872 }, |
|
873 |
|
874 QueryInterface: XPCOMUtils.generateQI([ |
|
875 Ci.mozILivemark |
|
876 ]) |
|
877 } |
|
878 |
|
879 //////////////////////////////////////////////////////////////////////////////// |
|
880 //// LivemarkLoadListener |
|
881 |
|
882 /** |
|
883 * Object used internally to handle loading a livemark's contents. |
|
884 * |
|
885 * @param aLivemark |
|
886 * The Livemark that is loading. |
|
887 */ |
|
888 function LivemarkLoadListener(aLivemark) |
|
889 { |
|
890 this._livemark = aLivemark; |
|
891 this._processor = null; |
|
892 this._isAborted = false; |
|
893 this._ttl = EXPIRE_TIME_MS; |
|
894 } |
|
895 |
|
896 LivemarkLoadListener.prototype = { |
|
897 abort: function LLL_abort(aException) |
|
898 { |
|
899 if (!this._isAborted) { |
|
900 this._isAborted = true; |
|
901 this._livemark.abort(); |
|
902 this._setResourceTTL(ONERROR_EXPIRE_TIME_MS); |
|
903 } |
|
904 }, |
|
905 |
|
906 // nsIFeedResultListener |
|
907 handleResult: function LLL_handleResult(aResult) |
|
908 { |
|
909 if (this._isAborted) { |
|
910 return; |
|
911 } |
|
912 |
|
913 try { |
|
914 // We need this to make sure the item links are safe |
|
915 let feedPrincipal = |
|
916 secMan.getSimpleCodebasePrincipal(this._livemark.feedURI); |
|
917 |
|
918 // Enforce well-formedness because the existing code does |
|
919 if (!aResult || !aResult.doc || aResult.bozo) { |
|
920 throw new Components.Exception("", Cr.NS_ERROR_FAILURE); |
|
921 } |
|
922 |
|
923 let feed = aResult.doc.QueryInterface(Ci.nsIFeed); |
|
924 let siteURI = this._livemark.siteURI; |
|
925 if (feed.link && (!siteURI || !feed.link.equals(siteURI))) { |
|
926 siteURI = feed.link; |
|
927 this._livemark.writeSiteURI(siteURI); |
|
928 } |
|
929 |
|
930 // Insert feed items. |
|
931 let livemarkChildren = []; |
|
932 for (let i = 0; i < feed.items.length; ++i) { |
|
933 let entry = feed.items.queryElementAt(i, Ci.nsIFeedEntry); |
|
934 let uri = entry.link || siteURI; |
|
935 if (!uri) { |
|
936 continue; |
|
937 } |
|
938 |
|
939 try { |
|
940 secMan.checkLoadURIWithPrincipal(feedPrincipal, uri, SEC_FLAGS); |
|
941 } |
|
942 catch(ex) { |
|
943 continue; |
|
944 } |
|
945 |
|
946 let title = entry.title ? entry.title.plainText() : ""; |
|
947 livemarkChildren.push({ uri: uri, title: title, visited: false }); |
|
948 } |
|
949 |
|
950 this._livemark.children = livemarkChildren; |
|
951 } |
|
952 catch (ex) { |
|
953 this.abort(ex); |
|
954 } |
|
955 finally { |
|
956 this._processor.listener = null; |
|
957 this._processor = null; |
|
958 } |
|
959 }, |
|
960 |
|
961 onDataAvailable: function LLL_onDataAvailable(aRequest, aContext, |
|
962 aInputStream, aSourceOffset, |
|
963 aCount) |
|
964 { |
|
965 if (this._processor) { |
|
966 this._processor.onDataAvailable(aRequest, aContext, aInputStream, |
|
967 aSourceOffset, aCount); |
|
968 } |
|
969 }, |
|
970 |
|
971 onStartRequest: function LLL_onStartRequest(aRequest, aContext) |
|
972 { |
|
973 if (this._isAborted) { |
|
974 throw Cr.NS_ERROR_UNEXPECTED; |
|
975 } |
|
976 |
|
977 let channel = aRequest.QueryInterface(Ci.nsIChannel); |
|
978 try { |
|
979 // Parse feed data as it comes in |
|
980 this._processor = Cc["@mozilla.org/feed-processor;1"]. |
|
981 createInstance(Ci.nsIFeedProcessor); |
|
982 this._processor.listener = this; |
|
983 this._processor.parseAsync(null, channel.URI); |
|
984 this._processor.onStartRequest(aRequest, aContext); |
|
985 } |
|
986 catch (ex) { |
|
987 Components.utils.reportError("Livemark Service: feed processor received an invalid channel for " + channel.URI.spec); |
|
988 this.abort(ex); |
|
989 } |
|
990 }, |
|
991 |
|
992 onStopRequest: function LLL_onStopRequest(aRequest, aContext, aStatus) |
|
993 { |
|
994 if (!Components.isSuccessCode(aStatus)) { |
|
995 this.abort(); |
|
996 return; |
|
997 } |
|
998 |
|
999 // Set an expiration on the livemark, to reloading the data in future. |
|
1000 try { |
|
1001 if (this._processor) { |
|
1002 this._processor.onStopRequest(aRequest, aContext, aStatus); |
|
1003 } |
|
1004 |
|
1005 // Calculate a new ttl |
|
1006 let channel = aRequest.QueryInterface(Ci.nsICachingChannel); |
|
1007 if (channel) { |
|
1008 let entryInfo = channel.cacheToken.QueryInterface(Ci.nsICacheEntry); |
|
1009 if (entryInfo) { |
|
1010 // nsICacheEntry returns value as seconds. |
|
1011 let expireTime = entryInfo.expirationTime * 1000; |
|
1012 let nowTime = Date.now(); |
|
1013 // Note, expireTime can be 0, see bug 383538. |
|
1014 if (expireTime > nowTime) { |
|
1015 this._setResourceTTL(Math.max((expireTime - nowTime), |
|
1016 EXPIRE_TIME_MS)); |
|
1017 return; |
|
1018 } |
|
1019 } |
|
1020 } |
|
1021 this._setResourceTTL(EXPIRE_TIME_MS); |
|
1022 } |
|
1023 catch (ex) { |
|
1024 this.abort(ex); |
|
1025 } |
|
1026 finally { |
|
1027 if (this._livemark.status == Ci.mozILivemark.STATUS_LOADING) { |
|
1028 this._livemark.status = Ci.mozILivemark.STATUS_READY; |
|
1029 } |
|
1030 this._livemark.locked = false; |
|
1031 this._livemark.loadGroup = null; |
|
1032 } |
|
1033 }, |
|
1034 |
|
1035 _setResourceTTL: function LLL__setResourceTTL(aMilliseconds) |
|
1036 { |
|
1037 this._livemark.expireTime = Date.now() + aMilliseconds; |
|
1038 }, |
|
1039 |
|
1040 // nsIInterfaceRequestor |
|
1041 getInterface: function LLL_getInterface(aIID) |
|
1042 { |
|
1043 return this.QueryInterface(aIID); |
|
1044 }, |
|
1045 |
|
1046 // nsISupports |
|
1047 QueryInterface: XPCOMUtils.generateQI([ |
|
1048 Ci.nsIFeedResultListener |
|
1049 , Ci.nsIStreamListener |
|
1050 , Ci.nsIRequestObserver |
|
1051 , Ci.nsIInterfaceRequestor |
|
1052 ]) |
|
1053 } |
|
1054 |
|
1055 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LivemarkService]); |