Sat, 03 Jan 2015 20:18:00 +0100
Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.
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/. */
5 const Cc = Components.classes;
6 const Ci = Components.interfaces;
7 const Cr = Components.results;
8 const Cu = Components.utils;
10 ////////////////////////////////////////////////////////////////////////////////
11 //// Modules
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");
24 ////////////////////////////////////////////////////////////////////////////////
25 //// Services
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 });
36 ////////////////////////////////////////////////////////////////////////////////
37 //// Constants
39 // Security flags for checkLoadURIWithPrincipal.
40 const SEC_FLAGS = Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL;
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.
49 ////////////////////////////////////////////////////////////////////////////////
50 //// LivemarkService
52 function LivemarkService()
53 {
54 // Cleanup on shutdown.
55 Services.obs.addObserver(this, PlacesUtils.TOPIC_SHUTDOWN, true);
57 // Observe bookmarks and history, but don't init the services just for that.
58 PlacesUtils.addLazyBookmarkObserver(this, true);
60 // Asynchronously build the livemarks cache.
61 this._ensureAsynchronousCache();
62 }
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: {},
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 }
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 },
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;
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 },
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 },
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 },
174 //////////////////////////////////////////////////////////////////////////////
175 //// nsIObserver
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 }
187 if (this._reloadTimer) {
188 this._reloading = false;
189 this._reloadTimer.cancel();
190 delete this._reloadTimer;
191 }
193 // Stop any ongoing update.
194 for each (let livemark in this._livemarks) {
195 livemark.terminate();
196 }
197 this._livemarks = {};
198 }
199 },
201 //////////////////////////////////////////////////////////////////////////////
202 //// mozIAsyncLivemarks
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 }
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 }
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 }
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 }
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 }
286 return aLivemarkCallback ? null : deferred.promise;
287 },
289 removeLivemark: function LS_removeLivemark(aLivemarkInfo, aLivemarkCallback)
290 {
291 if (!aLivemarkInfo) {
292 throw Cr.NS_ERROR_INVALID_ARG;
293 }
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 }
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 }
309 // Convert the guid to an id.
310 if (id in this._guids) {
311 id = this._guids[id];
312 }
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 }
350 return aLivemarkCallback ? null : deferred.promise;
351 },
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 },
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 }
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 },
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 }
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 }
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 });
433 return aLivemarkCallback ? null : deferred.promise;
434 },
436 //////////////////////////////////////////////////////////////////////////////
437 //// nsINavBookmarkObserver
439 onBeginUpdateBatch: function () {},
440 onEndUpdateBatch: function () {},
441 onItemVisited: function () {},
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 },
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 },
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 },
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 },
493 //////////////////////////////////////////////////////////////////////////////
494 //// nsINavHistoryObserver
496 onBeginUpdateBatch: function () {},
497 onEndUpdateBatch: function () {},
498 onPageChanged: function () {},
499 onTitleChanged: function () {},
500 onDeleteVisits: function () {},
501 onClearHistory: function () {},
503 onDeleteURI: function PS_onDeleteURI(aURI) {
504 for each (let livemark in this._livemarks) {
505 livemark.updateURIVisitedStatus(aURI, false);
506 }
507 },
509 onVisit: function PS_onVisit(aURI) {
510 for each (let livemark in this._livemarks) {
511 livemark.updateURIVisitedStatus(aURI, true);
512 }
513 },
515 //////////////////////////////////////////////////////////////////////////////
516 //// nsISupports
518 classID: Components.ID("{dca61eb5-c7cd-4df1-b0fb-d0722baba251}"),
520 _xpcom_factory: XPCOMUtils.generateSingletonFactory(LivemarkService),
522 QueryInterface: XPCOMUtils.generateQI([
523 Ci.mozIAsyncLivemarks
524 , Ci.nsINavBookmarkObserver
525 , Ci.nsINavHistoryObserver
526 , Ci.nsIObserver
527 , Ci.nsISupportsWeakReference
528 ])
529 };
531 ////////////////////////////////////////////////////////////////////////////////
532 //// Livemark
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;
549 this._status = Ci.mozILivemark.STATUS_READY;
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 = [];
557 // Sorted array of objects representing livemark children in the form
558 // { uri, title, visited }.
559 this._children = [];
561 // Keeps a separate array of nodes for each requesting container, hashed by
562 // the container itself.
563 this._nodes = new Map();
565 this._guid = "";
566 this._lastModified = 0;
568 this.loadGroup = null;
569 this.feedURI = null;
570 this.siteURI = null;
571 this.expireTime = 0;
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 }
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 },
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 },
627 writeFeedURI: function LM_writeFeedURI(aFeedURI)
628 {
629 this._setAnno(PlacesUtils.LMANNO_FEEDURI, aFeedURI.spec);
630 this.feedURI = aFeedURI;
631 },
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 }
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 }
651 this._setAnno(PlacesUtils.LMANNO_SITEURI, aSiteURI.spec)
652 this.siteURI = aSiteURI;
653 },
655 set guid(aGUID) {
656 this._guid = aGUID;
657 return aGUID;
658 },
659 get guid() this._guid,
661 set lastModified(aLastModified) {
662 this._lastModified = aLastModified;
663 return aLastModified;
664 },
665 get lastModified() this._lastModified,
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;
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;
686 this.status = Ci.mozILivemark.STATUS_LOADING;
688 // Setting the status notifies observers that may remove the livemark.
689 if (this._terminated)
690 return;
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);
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);
711 this.loadGroup = loadgroup;
712 }
713 catch (ex) {
714 this.status = Ci.mozILivemark.STATUS_FAILED;
715 }
716 },
718 reload: function LM_reload(aForceUpdate)
719 {
720 this.updateChildren(aForceUpdate);
721 },
723 remove: function LM_remove() {
724 PlacesUtils.bookmarks.removeItem(this.id);
725 },
727 get children() this._children,
728 set children(val) {
729 this._children = val;
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 }
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 }
746 return this._children;
747 },
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 },
757 getNodesForContainer: function LM_getNodesForContainer(aContainerNode)
758 {
759 if (this._nodes.has(aContainerNode)) {
760 return this._nodes.get(aContainerNode);
761 }
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 },
797 registerForUpdates: function LM_registerForUpdates(aContainerNode,
798 aResultObserver)
799 {
800 this._resultObservers.set(aContainerNode, aResultObserver);
801 this._resultObserversList.push(aContainerNode);
802 },
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);
810 this._nodes.delete(aContainerNode);
811 },
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 },
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 }
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 },
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 },
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 },
874 QueryInterface: XPCOMUtils.generateQI([
875 Ci.mozILivemark
876 ])
877 }
879 ////////////////////////////////////////////////////////////////////////////////
880 //// LivemarkLoadListener
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 }
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 },
906 // nsIFeedResultListener
907 handleResult: function LLL_handleResult(aResult)
908 {
909 if (this._isAborted) {
910 return;
911 }
913 try {
914 // We need this to make sure the item links are safe
915 let feedPrincipal =
916 secMan.getSimpleCodebasePrincipal(this._livemark.feedURI);
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 }
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 }
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 }
939 try {
940 secMan.checkLoadURIWithPrincipal(feedPrincipal, uri, SEC_FLAGS);
941 }
942 catch(ex) {
943 continue;
944 }
946 let title = entry.title ? entry.title.plainText() : "";
947 livemarkChildren.push({ uri: uri, title: title, visited: false });
948 }
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 },
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 },
971 onStartRequest: function LLL_onStartRequest(aRequest, aContext)
972 {
973 if (this._isAborted) {
974 throw Cr.NS_ERROR_UNEXPECTED;
975 }
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 },
992 onStopRequest: function LLL_onStopRequest(aRequest, aContext, aStatus)
993 {
994 if (!Components.isSuccessCode(aStatus)) {
995 this.abort();
996 return;
997 }
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 }
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 },
1035 _setResourceTTL: function LLL__setResourceTTL(aMilliseconds)
1036 {
1037 this._livemark.expireTime = Date.now() + aMilliseconds;
1038 },
1040 // nsIInterfaceRequestor
1041 getInterface: function LLL_getInterface(aIID)
1042 {
1043 return this.QueryInterface(aIID);
1044 },
1046 // nsISupports
1047 QueryInterface: XPCOMUtils.generateQI([
1048 Ci.nsIFeedResultListener
1049 , Ci.nsIStreamListener
1050 , Ci.nsIRequestObserver
1051 , Ci.nsIInterfaceRequestor
1052 ])
1053 }
1055 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LivemarkService]);