Bug 1274108 - Move the "Livemark" class into a separate module. draft
authorKit Cambridge <kcambridge@mozilla.com>
Fri, 03 Jun 2016 11:13:12 -0700
changeset 375298 fbd10c11ef72925d542962673b3ba77a72ceeb0f
parent 374521 1092e4e8a4c7480e7091175bf58e2c92021de33c
child 375299 4384b8de2b3aa506150ea72cdcb15dff7a829edd
push id20219
push userkcambridge@mozilla.com
push dateFri, 03 Jun 2016 21:20:40 +0000
bugs1274108
milestone49.0a1
Bug 1274108 - Move the "Livemark" class into a separate module. MozReview-Commit-ID: 4zw4kF6IZVC
toolkit/components/places/Livemark.jsm
toolkit/components/places/moz.build
toolkit/components/places/nsLivemarkService.js
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/Livemark.jsm
@@ -0,0 +1,566 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+this.EXPORTED_SYMBOLS = ["Livemark", "LivemarksCache"];
+
+////////////////////////////////////////////////////////////////////////////////
+//// Modules and services.
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+                                  "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+                                  "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "asyncHistory", function () {
+  // Lazily add an history observer when it's actually needed.
+  PlacesUtils.history.addObserver(PlacesUtils.livemarks, true);
+  return PlacesUtils.asyncHistory;
+});
+
+////////////////////////////////////////////////////////////////////////////////
+//// Livemarks cache.
+
+XPCOMUtils.defineLazyGetter(this, "CACHE_SQL", () => {
+  function getAnnoSQLFragment(aAnnoParam) {
+    return `SELECT a.content
+            FROM moz_items_annos a
+            JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id
+            WHERE a.item_id = b.id
+              AND n.name = ${aAnnoParam}`;
+  }
+
+  return `SELECT b.id, b.title, b.parent As parentId, b.position AS 'index',
+                 b.guid, b.dateAdded, b.lastModified, p.guid AS parentGuid,
+                 ( ${getAnnoSQLFragment(":feedURI_anno")} ) AS feedURI,
+                 ( ${getAnnoSQLFragment(":siteURI_anno")} ) AS siteURI
+          FROM moz_bookmarks b
+          JOIN moz_bookmarks p ON b.parent = p.id
+          JOIN moz_items_annos a ON a.item_id = b.id
+          JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id
+          WHERE b.type = :folder_type
+            AND n.name = :feedURI_anno`;
+});
+
+XPCOMUtils.defineLazyGetter(this, "gLivemarksCachePromised",
+  Task.async(function* () {
+    let livemarksMap = new Map();
+    let conn = yield PlacesUtils.promiseDBConnection();
+    let rows = yield conn.executeCached(CACHE_SQL,
+      { folder_type: Ci.nsINavBookmarksService.TYPE_FOLDER,
+        feedURI_anno: PlacesUtils.LMANNO_FEEDURI,
+        siteURI_anno: PlacesUtils.LMANNO_SITEURI });
+    for (let row of rows) {
+      let siteURI = row.getResultByName("siteURI");
+      let livemark = new Livemark({
+        id: row.getResultByName("id"),
+        guid: row.getResultByName("guid"),
+        title: row.getResultByName("title"),
+        parentId: row.getResultByName("parentId"),
+        parentGuid: row.getResultByName("parentGuid"),
+        index: row.getResultByName("index"),
+        dateAdded: row.getResultByName("dateAdded"),
+        lastModified: row.getResultByName("lastModified"),
+        feedURI: NetUtil.newURI(row.getResultByName("feedURI")),
+        siteURI: siteURI ? NetUtil.newURI(siteURI) : null
+      });
+      livemarksMap.set(livemark.guid, livemark);
+    }
+    return livemarksMap;
+  })
+);
+
+var LivemarksCache = {
+  promiseLivemarksMap() {
+    return gLivemarksCachePromised;
+  },
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// Livemark
+
+/**
+ * Object used internally to represent a livemark.
+ *
+ * @param aLivemarkInfo
+ *        Object containing information on the livemark.  If the livemark is
+ *        not included in the object, a new livemark will be created.
+ *
+ * @note terminate() must be invoked before getting rid of this object.
+ */
+function Livemark(aLivemarkInfo)
+{
+  this.id = aLivemarkInfo.id;
+  this.guid = aLivemarkInfo.guid;
+  this.feedURI = aLivemarkInfo.feedURI;
+  this.siteURI = aLivemarkInfo.siteURI || null;
+  this.title = aLivemarkInfo.title;
+  this.parentId = aLivemarkInfo.parentId;
+  this.parentGuid = aLivemarkInfo.parentGuid;
+  this.index = aLivemarkInfo.index;
+  this.dateAdded = aLivemarkInfo.dateAdded;
+  this.lastModified = aLivemarkInfo.lastModified;
+
+  this._status = Ci.mozILivemark.STATUS_READY;
+
+  // Hash of resultObservers, hashed by container.
+  this._resultObservers = new Map();
+
+  // Sorted array of objects representing livemark children in the form
+  // { uri, title, visited }.
+  this._children = [];
+
+  // Keeps a separate array of nodes for each requesting container, hashed by
+  // the container itself.
+  this._nodes = new Map();
+
+  this.loadGroup = null;
+  this.expireTime = 0;
+}
+
+Livemark.prototype = {
+  get status() {
+    return this._status;
+  },
+  set status(val) {
+    if (this._status != val) {
+      this._status = val;
+      this._invalidateRegisteredContainers();
+    }
+    return this._status;
+  },
+
+  writeFeedURI(aFeedURI) {
+    PlacesUtils.annotations
+               .setItemAnnotation(this.id, PlacesUtils.LMANNO_FEEDURI,
+                                  aFeedURI.spec,
+                                  0, PlacesUtils.annotations.EXPIRE_NEVER);
+    this.feedURI = aFeedURI;
+  },
+
+  writeSiteURI(aSiteURI) {
+    if (!aSiteURI) {
+      PlacesUtils.annotations.removeItemAnnotation(this.id,
+                                                   PlacesUtils.LMANNO_SITEURI)
+      this.siteURI = null;
+      return;
+    }
+
+    // Security check the site URI against the feed URI principal.
+    let secMan = Services.scriptSecurityManager;
+    let feedPrincipal = secMan.createCodebasePrincipal(this.feedURI, {});
+    try {
+      secMan.checkLoadURIWithPrincipal(feedPrincipal, aSiteURI,
+                                       Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
+    }
+    catch (ex) {
+      return;
+    }
+
+    PlacesUtils.annotations
+               .setItemAnnotation(this.id, PlacesUtils.LMANNO_SITEURI,
+                                  aSiteURI.spec,
+                                  0, PlacesUtils.annotations.EXPIRE_NEVER);
+    this.siteURI = aSiteURI;
+  },
+
+  /**
+   * Tries to updates the livemark if needed.
+   * The update process is asynchronous.
+   *
+   * @param [optional] aForceUpdate
+   *        If true will try to update the livemark even if its contents have
+   *        not yet expired.
+   */
+  updateChildren(aForceUpdate) {
+    // Check if the livemark is already updating.
+    if (this.status == Ci.mozILivemark.STATUS_LOADING)
+      return;
+
+    // Check the TTL/expiration on this, to check if there is no need to update
+    // this livemark.
+    if (!aForceUpdate && this.children.length && this.expireTime > Date.now())
+      return;
+
+    this.status = Ci.mozILivemark.STATUS_LOADING;
+
+    // Setting the status notifies observers that may remove the livemark.
+    if (this._terminated)
+      return;
+
+    try {
+      // Create a load group for the request.  This will allow us to
+      // automatically keep track of redirects, so we can always
+      // cancel the channel.
+      let loadgroup = Cc["@mozilla.org/network/load-group;1"].
+                      createInstance(Ci.nsILoadGroup);
+      // Creating a CodeBasePrincipal and using it as the loadingPrincipal
+      // is *not* desired and is only tolerated within this file.
+      // TODO: Find the right OriginAttributes and pass something other
+      // than {} to .createCodeBasePrincipal().
+      let channel = NetUtil.newChannel({
+        uri: this.feedURI,
+        loadingPrincipal: Services.scriptSecurityManager.createCodebasePrincipal(this.feedURI, {}),
+        securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+        contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_XMLHTTPREQUEST
+      }).QueryInterface(Ci.nsIHttpChannel);
+      channel.loadGroup = loadgroup;
+      channel.loadFlags |= Ci.nsIRequest.LOAD_BACKGROUND |
+                           Ci.nsIRequest.LOAD_BYPASS_CACHE;
+      channel.requestMethod = "GET";
+      channel.setRequestHeader("X-Moz", "livebookmarks", false);
+
+      // Stream the result to the feed parser with this listener
+      let listener = new LivemarkLoadListener(this);
+      channel.notificationCallbacks = listener;
+      channel.asyncOpen2(listener);
+
+      this.loadGroup = loadgroup;
+    }
+    catch (ex) {
+      this.status = Ci.mozILivemark.STATUS_FAILED;
+    }
+  },
+
+  reload(aForceUpdate) {
+    this.updateChildren(aForceUpdate);
+  },
+
+  get children() {
+    return this._children;
+  },
+  set children(val) {
+    this._children = val;
+
+    // Discard the previous cached nodes, new ones should be generated.
+    for (let container of this._resultObservers.keys()) {
+      this._nodes.delete(container);
+    }
+
+    // Update visited status for each entry.
+    for (let child of this._children) {
+      asyncHistory.isURIVisited(child.uri, (aURI, aIsVisited) => {
+        this.updateURIVisitedStatus(aURI, aIsVisited);
+      });
+    }
+
+    return this._children;
+  },
+
+  _isURIVisited(aURI) {
+    return this.children.some(child => child.uri.equals(aURI) && child.visited);
+  },
+
+  getNodesForContainer(aContainerNode) {
+    if (this._nodes.has(aContainerNode)) {
+      return this._nodes.get(aContainerNode);
+    }
+
+    let livemark = this;
+    let nodes = [];
+    let now = Date.now() * 1000;
+    for (let child of this.children) {
+      // Workaround for bug 449811.
+      let localChild = child;
+      let node = {
+        // The QueryInterface is needed cause aContainerNode is a jsval.
+        // This is required to avoid issues with scriptable wrappers that would
+        // not allow the view to correctly set expandos.
+        get parent() {
+          return aContainerNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+        },
+        get parentResult() {
+          return this.parent.parentResult;
+        },
+        get uri() {
+          return localChild.uri.spec;
+        },
+        get type() {
+          return Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
+        },
+        get title() {
+          return localChild.title;
+        },
+        get accessCount() {
+          return Number(livemark._isURIVisited(NetUtil.newURI(this.uri)));
+        },
+        get time() {
+          return 0;
+        },
+        get icon() {
+          return "";
+        },
+        get indentLevel() {
+          return this.parent.indentLevel + 1;
+        },
+        get bookmarkIndex() {
+            return -1;
+        },
+        get itemId() {
+            return -1;
+        },
+        get dateAdded() {
+          return now;
+        },
+        get lastModified() {
+          return now;
+        },
+        get tags() {
+          return PlacesUtils.tagging.getTagsForURI(NetUtil.newURI(this.uri)).join(", ");
+        },
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryResultNode])
+      };
+      nodes.push(node);
+    }
+    this._nodes.set(aContainerNode, nodes);
+    return nodes;
+  },
+
+  registerForUpdates(aContainerNode, aResultObserver) {
+    this._resultObservers.set(aContainerNode, aResultObserver);
+  },
+
+  unregisterForUpdates(aContainerNode) {
+    this._resultObservers.delete(aContainerNode);
+    this._nodes.delete(aContainerNode);
+  },
+
+  _invalidateRegisteredContainers() {
+    for (let [ container, observer ] of this._resultObservers) {
+      observer.invalidateContainer(container);
+    }
+  },
+
+  /**
+   * Updates the visited status of nodes observing this livemark.
+   *
+   * @param aURI
+   *        If provided will update nodes having the given uri,
+   *        otherwise any node.
+   * @param aVisitedStatus
+   *        Whether the nodes should be set as visited.
+   */
+  updateURIVisitedStatus(aURI, aVisitedStatus) {
+    for (let child of this.children) {
+      if (!aURI || child.uri.equals(aURI)) {
+        child.visited = aVisitedStatus;
+      }
+    }
+
+    for (let [ container, observer ] of this._resultObservers) {
+      if (this._nodes.has(container)) {
+        let nodes = this._nodes.get(container);
+        for (let node of nodes) {
+          // Workaround for bug 449811.
+          let localObserver = observer;
+          let localNode = node;
+          if (!aURI || node.uri == aURI.spec) {
+            Services.tm.mainThread.dispatch(() => {
+              localObserver.nodeHistoryDetailsChanged(localNode, 0, aVisitedStatus);
+            }, Ci.nsIThread.DISPATCH_NORMAL);
+          }
+        }
+      }
+    }
+  },
+
+  /**
+   * Terminates the livemark entry, cancelling any ongoing load.
+   * Must be invoked before destroying the entry.
+   */
+  terminate() {
+    // Avoid handling any updateChildren request from now on.
+    this._terminated = true;
+    this.abort();
+  },
+
+  /**
+   * Aborts the livemark loading if needed.
+   */
+  abort() {
+    this.status = Ci.mozILivemark.STATUS_FAILED;
+    if (this.loadGroup) {
+      this.loadGroup.cancel(Cr.NS_BINDING_ABORTED);
+      this.loadGroup = null;
+    }
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.mozILivemark
+  ])
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// LivemarkLoadListener
+
+/**
+ * Object used internally to handle loading a livemark's contents.
+ *
+ * @param aLivemark
+ *        The Livemark that is loading.
+ */
+function LivemarkLoadListener(aLivemark) {
+  this._livemark = aLivemark;
+  this._processor = null;
+  this._isAborted = false;
+  this._ttl = EXPIRE_TIME_MS;
+}
+
+LivemarkLoadListener.prototype = {
+  abort(aException) {
+    if (!this._isAborted) {
+      this._isAborted = true;
+      this._livemark.abort();
+      this._setResourceTTL(ONERROR_EXPIRE_TIME_MS);
+    }
+  },
+
+  // nsIFeedResultListener
+  handleResult(aResult) {
+    if (this._isAborted) {
+      return;
+    }
+
+    try {
+      // We need this to make sure the item links are safe
+      let feedPrincipal =
+        Services.scriptSecurityManager
+                .createCodebasePrincipal(this._livemark.feedURI, {});
+
+      // Enforce well-formedness because the existing code does
+      if (!aResult || !aResult.doc || aResult.bozo) {
+        throw new Components.Exception("", Cr.NS_ERROR_FAILURE);
+      }
+
+      let feed = aResult.doc.QueryInterface(Ci.nsIFeed);
+      let siteURI = this._livemark.siteURI;
+      if (feed.link && (!siteURI || !feed.link.equals(siteURI))) {
+        siteURI = feed.link;
+        this._livemark.writeSiteURI(siteURI);
+      }
+
+      // Insert feed items.
+      let livemarkChildren = [];
+      for (let i = 0; i < feed.items.length; ++i) {
+        let entry = feed.items.queryElementAt(i, Ci.nsIFeedEntry);
+        let uri = entry.link || siteURI;
+        if (!uri) {
+          continue;
+        }
+
+        try {
+          Services.scriptSecurityManager
+                  .checkLoadURIWithPrincipal(feedPrincipal, uri,
+                                             Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
+        }
+        catch(ex) {
+          continue;
+        }
+
+        let title = entry.title ? entry.title.plainText() : "";
+        livemarkChildren.push({ uri: uri, title: title, visited: false });
+      }
+
+      this._livemark.children = livemarkChildren;
+    }
+    catch (ex) {
+      this.abort(ex);
+    }
+    finally {
+      this._processor.listener = null;
+      this._processor = null;
+    }
+  },
+
+  onDataAvailable(aRequest, aContext, aInputStream, aSourceOffset, aCount) {
+    if (this._processor) {
+      this._processor.onDataAvailable(aRequest, aContext, aInputStream,
+                                      aSourceOffset, aCount);
+    }
+  },
+
+  onStartRequest(aRequest, aContext) {
+    if (this._isAborted) {
+      throw new Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+    }
+
+    let channel = aRequest.QueryInterface(Ci.nsIChannel);
+    try {
+      // Parse feed data as it comes in
+      this._processor = Cc["@mozilla.org/feed-processor;1"].
+                        createInstance(Ci.nsIFeedProcessor);
+      this._processor.listener = this;
+      this._processor.parseAsync(null, channel.URI);
+      this._processor.onStartRequest(aRequest, aContext);
+    }
+    catch (ex) {
+      Components.utils.reportError("Livemark Service: feed processor received an invalid channel for " + channel.URI.spec);
+      this.abort(ex);
+    }
+  },
+
+  onStopRequest(aRequest, aContext, aStatus) {
+    if (!Components.isSuccessCode(aStatus)) {
+      this.abort();
+      return;
+    }
+
+    // Set an expiration on the livemark, to reloading the data in future.
+    try {
+      if (this._processor) {
+        this._processor.onStopRequest(aRequest, aContext, aStatus);
+      }
+
+      // Calculate a new ttl
+      let channel = aRequest.QueryInterface(Ci.nsICachingChannel);
+      if (channel) {
+        let entryInfo = channel.cacheToken.QueryInterface(Ci.nsICacheEntry);
+        if (entryInfo) {
+          // nsICacheEntry returns value as seconds.
+          let expireTime = entryInfo.expirationTime * 1000;
+          let nowTime = Date.now();
+          // Note, expireTime can be 0, see bug 383538.
+          if (expireTime > nowTime) {
+            this._setResourceTTL(Math.max((expireTime - nowTime),
+                                          EXPIRE_TIME_MS));
+            return;
+          }
+        }
+      }
+      this._setResourceTTL(EXPIRE_TIME_MS);
+    }
+    catch (ex) {
+      this.abort(ex);
+    }
+    finally {
+      if (this._livemark.status == Ci.mozILivemark.STATUS_LOADING) {
+        this._livemark.status = Ci.mozILivemark.STATUS_READY;
+      }
+      this._livemark.locked = false;
+      this._livemark.loadGroup = null;
+    }
+  },
+
+  _setResourceTTL(aMilliseconds) {
+    this._livemark.expireTime = Date.now() + aMilliseconds;
+  },
+
+  // nsIInterfaceRequestor
+  getInterface(aIID) {
+    return this.QueryInterface(aIID);
+  },
+
+  // nsISupports
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIFeedResultListener
+  , Ci.nsIStreamListener
+  , Ci.nsIRequestObserver
+  , Ci.nsIInterfaceRequestor
+  ])
+}
--- a/toolkit/components/places/moz.build
+++ b/toolkit/components/places/moz.build
@@ -60,16 +60,17 @@ if CONFIG['MOZ_PLACES']:
     EXTRA_JS_MODULES += [
         'BookmarkHTMLUtils.jsm',
         'BookmarkJSONUtils.jsm',
         'Bookmarks.jsm',
         'ClusterLib.js',
         'ColorAnalyzer_worker.js',
         'ColorConversion.js',
         'History.jsm',
+        'Livemark.jsm',
         'PlacesBackups.jsm',
         'PlacesDBUtils.jsm',
         'PlacesRemoteTabsAutocompleteProvider.jsm',
         'PlacesSearchAutocompleteProvider.jsm',
         'PlacesTransactions.jsm',
         'PlacesUtils.jsm',
     ]
 
--- a/toolkit/components/places/nsLivemarkService.js
+++ b/toolkit/components/places/nsLivemarkService.js
@@ -4,93 +4,39 @@
 
 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Modules and services.
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Livemark",
+                                  "resource://gre/modules/Livemark.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LivemarksCache",
+                                  "resource://gre/modules/Livemark.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
                                   "resource://gre/modules/Deprecated.jsm");
 
-XPCOMUtils.defineLazyGetter(this, "asyncHistory", function () {
-  // Lazily add an history observer when it's actually needed.
-  PlacesUtils.history.addObserver(PlacesUtils.livemarks, true);
-  return PlacesUtils.asyncHistory;
-});
-
 ////////////////////////////////////////////////////////////////////////////////
 //// Constants
 
 // Delay between reloads of consecute livemarks.
 const RELOAD_DELAY_MS = 500;
 // Expire livemarks after this time.
 const EXPIRE_TIME_MS = 3600000; // 1 hour.
 // Expire livemarks after this time on error.
 const ONERROR_EXPIRE_TIME_MS = 300000; // 5 minutes.
 
-////////////////////////////////////////////////////////////////////////////////
-//// Livemarks cache.
-
-XPCOMUtils.defineLazyGetter(this, "CACHE_SQL", () => {
-  function getAnnoSQLFragment(aAnnoParam) {
-    return `SELECT a.content
-            FROM moz_items_annos a
-            JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id
-            WHERE a.item_id = b.id
-              AND n.name = ${aAnnoParam}`;
-  }
-
-  return `SELECT b.id, b.title, b.parent As parentId, b.position AS 'index',
-                 b.guid, b.dateAdded, b.lastModified, p.guid AS parentGuid,
-                 ( ${getAnnoSQLFragment(":feedURI_anno")} ) AS feedURI,
-                 ( ${getAnnoSQLFragment(":siteURI_anno")} ) AS siteURI
-          FROM moz_bookmarks b
-          JOIN moz_bookmarks p ON b.parent = p.id
-          JOIN moz_items_annos a ON a.item_id = b.id
-          JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id
-          WHERE b.type = :folder_type
-            AND n.name = :feedURI_anno`;
-});
-
-XPCOMUtils.defineLazyGetter(this, "gLivemarksCachePromised",
-  Task.async(function* () {
-    let livemarksMap = new Map();
-    let conn = yield PlacesUtils.promiseDBConnection();
-    let rows = yield conn.executeCached(CACHE_SQL,
-      { folder_type: Ci.nsINavBookmarksService.TYPE_FOLDER,
-        feedURI_anno: PlacesUtils.LMANNO_FEEDURI,
-        siteURI_anno: PlacesUtils.LMANNO_SITEURI });
-    for (let row of rows) {
-      let siteURI = row.getResultByName("siteURI");
-      let livemark = new Livemark({
-        id: row.getResultByName("id"),
-        guid: row.getResultByName("guid"),
-        title: row.getResultByName("title"),
-        parentId: row.getResultByName("parentId"),
-        parentGuid: row.getResultByName("parentGuid"),
-        index: row.getResultByName("index"),
-        dateAdded: row.getResultByName("dateAdded"),
-        lastModified: row.getResultByName("lastModified"),
-        feedURI: NetUtil.newURI(row.getResultByName("feedURI")),
-        siteURI: siteURI ? NetUtil.newURI(siteURI) : null
-      });
-      livemarksMap.set(livemark.guid, livemark);
-    }
-    return livemarksMap;
-  })
-);
-
 /**
  * Convert a Date object to a PRTime (microseconds).
  *
  * @param date
  *        the Date object to convert.
  * @return microseconds from the epoch.
  */
 function toPRTime(date) {
@@ -116,17 +62,17 @@ function LivemarkService() {
   Services.obs.addObserver(this, PlacesUtils.TOPIC_SHUTDOWN, true);
 
   // Observe bookmarks but don't init the service just for that.
   PlacesUtils.addLazyBookmarkObserver(this, true);
 }
 
 LivemarkService.prototype = {
   // This is just an helper for code readability.
-  _promiseLivemarksMap: () => gLivemarksCachePromised,
+  _promiseLivemarksMap: () => LivemarksCache.promiseLivemarksMap(),
 
   _reloading: false,
   _startReloadTimer(livemarksMap, forceUpdate, reloaded) {
     if (this._reloadTimer) {
       this._reloadTimer.cancel();
     }
     else {
       this._reloadTimer = Cc["@mozilla.org/timer;1"]
@@ -405,492 +351,9 @@ LivemarkService.prototype = {
     Ci.mozIAsyncLivemarks
   , Ci.nsINavBookmarkObserver
   , Ci.nsINavHistoryObserver
   , Ci.nsIObserver
   , Ci.nsISupportsWeakReference
   ])
 };
 
-////////////////////////////////////////////////////////////////////////////////
-//// Livemark
-
-/**
- * Object used internally to represent a livemark.
- *
- * @param aLivemarkInfo
- *        Object containing information on the livemark.  If the livemark is
- *        not included in the object, a new livemark will be created.
- *
- * @note terminate() must be invoked before getting rid of this object.
- */
-function Livemark(aLivemarkInfo)
-{
-  this.id = aLivemarkInfo.id;
-  this.guid = aLivemarkInfo.guid;
-  this.feedURI = aLivemarkInfo.feedURI;
-  this.siteURI = aLivemarkInfo.siteURI || null;
-  this.title = aLivemarkInfo.title;
-  this.parentId = aLivemarkInfo.parentId;
-  this.parentGuid = aLivemarkInfo.parentGuid;
-  this.index = aLivemarkInfo.index;
-  this.dateAdded = aLivemarkInfo.dateAdded;
-  this.lastModified = aLivemarkInfo.lastModified;
-
-  this._status = Ci.mozILivemark.STATUS_READY;
-
-  // Hash of resultObservers, hashed by container.
-  this._resultObservers = new Map();
-
-  // Sorted array of objects representing livemark children in the form
-  // { uri, title, visited }.
-  this._children = [];
-
-  // Keeps a separate array of nodes for each requesting container, hashed by
-  // the container itself.
-  this._nodes = new Map();
-
-  this.loadGroup = null;
-  this.expireTime = 0;
-}
-
-Livemark.prototype = {
-  get status() {
-    return this._status;
-  },
-  set status(val) {
-    if (this._status != val) {
-      this._status = val;
-      this._invalidateRegisteredContainers();
-    }
-    return this._status;
-  },
-
-  writeFeedURI(aFeedURI) {
-    PlacesUtils.annotations
-               .setItemAnnotation(this.id, PlacesUtils.LMANNO_FEEDURI,
-                                  aFeedURI.spec,
-                                  0, PlacesUtils.annotations.EXPIRE_NEVER);
-    this.feedURI = aFeedURI;
-  },
-
-  writeSiteURI(aSiteURI) {
-    if (!aSiteURI) {
-      PlacesUtils.annotations.removeItemAnnotation(this.id,
-                                                   PlacesUtils.LMANNO_SITEURI)
-      this.siteURI = null;
-      return;
-    }
-
-    // Security check the site URI against the feed URI principal.
-    let secMan = Services.scriptSecurityManager;
-    let feedPrincipal = secMan.createCodebasePrincipal(this.feedURI, {});
-    try {
-      secMan.checkLoadURIWithPrincipal(feedPrincipal, aSiteURI,
-                                       Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
-    }
-    catch (ex) {
-      return;
-    }
-
-    PlacesUtils.annotations
-               .setItemAnnotation(this.id, PlacesUtils.LMANNO_SITEURI,
-                                  aSiteURI.spec,
-                                  0, PlacesUtils.annotations.EXPIRE_NEVER);
-    this.siteURI = aSiteURI;
-  },
-
-  /**
-   * Tries to updates the livemark if needed.
-   * The update process is asynchronous.
-   *
-   * @param [optional] aForceUpdate
-   *        If true will try to update the livemark even if its contents have
-   *        not yet expired.
-   */
-  updateChildren(aForceUpdate) {
-    // Check if the livemark is already updating.
-    if (this.status == Ci.mozILivemark.STATUS_LOADING)
-      return;
-
-    // Check the TTL/expiration on this, to check if there is no need to update
-    // this livemark.
-    if (!aForceUpdate && this.children.length && this.expireTime > Date.now())
-      return;
-
-    this.status = Ci.mozILivemark.STATUS_LOADING;
-
-    // Setting the status notifies observers that may remove the livemark.
-    if (this._terminated)
-      return;
-
-    try {
-      // Create a load group for the request.  This will allow us to
-      // automatically keep track of redirects, so we can always
-      // cancel the channel.
-      let loadgroup = Cc["@mozilla.org/network/load-group;1"].
-                      createInstance(Ci.nsILoadGroup);
-      // Creating a CodeBasePrincipal and using it as the loadingPrincipal
-      // is *not* desired and is only tolerated within this file.
-      // TODO: Find the right OriginAttributes and pass something other
-      // than {} to .createCodeBasePrincipal().
-      let channel = NetUtil.newChannel({
-        uri: this.feedURI,
-        loadingPrincipal: Services.scriptSecurityManager.createCodebasePrincipal(this.feedURI, {}),
-        securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
-        contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_XMLHTTPREQUEST
-      }).QueryInterface(Ci.nsIHttpChannel);
-      channel.loadGroup = loadgroup;
-      channel.loadFlags |= Ci.nsIRequest.LOAD_BACKGROUND |
-                           Ci.nsIRequest.LOAD_BYPASS_CACHE;
-      channel.requestMethod = "GET";
-      channel.setRequestHeader("X-Moz", "livebookmarks", false);
-
-      // Stream the result to the feed parser with this listener
-      let listener = new LivemarkLoadListener(this);
-      channel.notificationCallbacks = listener;
-      channel.asyncOpen2(listener);
-
-      this.loadGroup = loadgroup;
-    }
-    catch (ex) {
-      this.status = Ci.mozILivemark.STATUS_FAILED;
-    }
-  },
-
-  reload(aForceUpdate) {
-    this.updateChildren(aForceUpdate);
-  },
-
-  get children() {
-    return this._children;
-  },
-  set children(val) {
-    this._children = val;
-
-    // Discard the previous cached nodes, new ones should be generated.
-    for (let container of this._resultObservers.keys()) {
-      this._nodes.delete(container);
-    }
-
-    // Update visited status for each entry.
-    for (let child of this._children) {
-      asyncHistory.isURIVisited(child.uri, (aURI, aIsVisited) => {
-        this.updateURIVisitedStatus(aURI, aIsVisited);
-      });
-    }
-
-    return this._children;
-  },
-
-  _isURIVisited(aURI) {
-    return this.children.some(child => child.uri.equals(aURI) && child.visited);
-  },
-
-  getNodesForContainer(aContainerNode) {
-    if (this._nodes.has(aContainerNode)) {
-      return this._nodes.get(aContainerNode);
-    }
-
-    let livemark = this;
-    let nodes = [];
-    let now = Date.now() * 1000;
-    for (let child of this.children) {
-      // Workaround for bug 449811.
-      let localChild = child;
-      let node = {
-        // The QueryInterface is needed cause aContainerNode is a jsval.
-        // This is required to avoid issues with scriptable wrappers that would
-        // not allow the view to correctly set expandos.
-        get parent() {
-          return aContainerNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
-        },
-        get parentResult() {
-          return this.parent.parentResult;
-        },
-        get uri() {
-          return localChild.uri.spec;
-        },
-        get type() {
-          return Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
-        },
-        get title() {
-          return localChild.title;
-        },
-        get accessCount() {
-          return Number(livemark._isURIVisited(NetUtil.newURI(this.uri)));
-        },
-        get time() {
-          return 0;
-        },
-        get icon() {
-          return "";
-        },
-        get indentLevel() {
-          return this.parent.indentLevel + 1;
-        },
-        get bookmarkIndex() {
-            return -1;
-        },
-        get itemId() {
-            return -1;
-        },
-        get dateAdded() {
-          return now;
-        },
-        get lastModified() {
-          return now;
-        },
-        get tags() {
-          return PlacesUtils.tagging.getTagsForURI(NetUtil.newURI(this.uri)).join(", ");
-        },
-        QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryResultNode])
-      };
-      nodes.push(node);
-    }
-    this._nodes.set(aContainerNode, nodes);
-    return nodes;
-  },
-
-  registerForUpdates(aContainerNode, aResultObserver) {
-    this._resultObservers.set(aContainerNode, aResultObserver);
-  },
-
-  unregisterForUpdates(aContainerNode) {
-    this._resultObservers.delete(aContainerNode);
-    this._nodes.delete(aContainerNode);
-  },
-
-  _invalidateRegisteredContainers() {
-    for (let [ container, observer ] of this._resultObservers) {
-      observer.invalidateContainer(container);
-    }
-  },
-
-  /**
-   * Updates the visited status of nodes observing this livemark.
-   *
-   * @param aURI
-   *        If provided will update nodes having the given uri,
-   *        otherwise any node.
-   * @param aVisitedStatus
-   *        Whether the nodes should be set as visited.
-   */
-  updateURIVisitedStatus(aURI, aVisitedStatus) {
-    for (let child of this.children) {
-      if (!aURI || child.uri.equals(aURI)) {
-        child.visited = aVisitedStatus;
-      }
-    }
-
-    for (let [ container, observer ] of this._resultObservers) {
-      if (this._nodes.has(container)) {
-        let nodes = this._nodes.get(container);
-        for (let node of nodes) {
-          // Workaround for bug 449811.
-          let localObserver = observer;
-          let localNode = node;
-          if (!aURI || node.uri == aURI.spec) {
-            Services.tm.mainThread.dispatch(() => {
-              localObserver.nodeHistoryDetailsChanged(localNode, 0, aVisitedStatus);
-            }, Ci.nsIThread.DISPATCH_NORMAL);
-          }
-        }
-      }
-    }
-  },
-
-  /**
-   * Terminates the livemark entry, cancelling any ongoing load.
-   * Must be invoked before destroying the entry.
-   */
-  terminate() {
-    // Avoid handling any updateChildren request from now on.
-    this._terminated = true;
-    this.abort();
-  },
-
-  /**
-   * Aborts the livemark loading if needed.
-   */
-  abort() {
-    this.status = Ci.mozILivemark.STATUS_FAILED;
-    if (this.loadGroup) {
-      this.loadGroup.cancel(Cr.NS_BINDING_ABORTED);
-      this.loadGroup = null;
-    }
-  },
-
-  QueryInterface: XPCOMUtils.generateQI([
-    Ci.mozILivemark
-  ])
-}
-
-////////////////////////////////////////////////////////////////////////////////
-//// LivemarkLoadListener
-
-/**
- * Object used internally to handle loading a livemark's contents.
- *
- * @param aLivemark
- *        The Livemark that is loading.
- */
-function LivemarkLoadListener(aLivemark) {
-  this._livemark = aLivemark;
-  this._processor = null;
-  this._isAborted = false;
-  this._ttl = EXPIRE_TIME_MS;
-}
-
-LivemarkLoadListener.prototype = {
-  abort(aException) {
-    if (!this._isAborted) {
-      this._isAborted = true;
-      this._livemark.abort();
-      this._setResourceTTL(ONERROR_EXPIRE_TIME_MS);
-    }
-  },
-
-  // nsIFeedResultListener
-  handleResult(aResult) {
-    if (this._isAborted) {
-      return;
-    }
-
-    try {
-      // We need this to make sure the item links are safe
-      let feedPrincipal =
-        Services.scriptSecurityManager
-                .createCodebasePrincipal(this._livemark.feedURI, {});
-
-      // Enforce well-formedness because the existing code does
-      if (!aResult || !aResult.doc || aResult.bozo) {
-        throw new Components.Exception("", Cr.NS_ERROR_FAILURE);
-      }
-
-      let feed = aResult.doc.QueryInterface(Ci.nsIFeed);
-      let siteURI = this._livemark.siteURI;
-      if (feed.link && (!siteURI || !feed.link.equals(siteURI))) {
-        siteURI = feed.link;
-        this._livemark.writeSiteURI(siteURI);
-      }
-
-      // Insert feed items.
-      let livemarkChildren = [];
-      for (let i = 0; i < feed.items.length; ++i) {
-        let entry = feed.items.queryElementAt(i, Ci.nsIFeedEntry);
-        let uri = entry.link || siteURI;
-        if (!uri) {
-          continue;
-        }
-
-        try {
-          Services.scriptSecurityManager
-                  .checkLoadURIWithPrincipal(feedPrincipal, uri,
-                                             Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
-        }
-        catch(ex) {
-          continue;
-        }
-
-        let title = entry.title ? entry.title.plainText() : "";
-        livemarkChildren.push({ uri: uri, title: title, visited: false });
-      }
-
-      this._livemark.children = livemarkChildren;
-    }
-    catch (ex) {
-      this.abort(ex);
-    }
-    finally {
-      this._processor.listener = null;
-      this._processor = null;
-    }
-  },
-
-  onDataAvailable(aRequest, aContext, aInputStream, aSourceOffset, aCount) {
-    if (this._processor) {
-      this._processor.onDataAvailable(aRequest, aContext, aInputStream,
-                                      aSourceOffset, aCount);
-    }
-  },
-
-  onStartRequest(aRequest, aContext) {
-    if (this._isAborted) {
-      throw new Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
-    }
-
-    let channel = aRequest.QueryInterface(Ci.nsIChannel);
-    try {
-      // Parse feed data as it comes in
-      this._processor = Cc["@mozilla.org/feed-processor;1"].
-                        createInstance(Ci.nsIFeedProcessor);
-      this._processor.listener = this;
-      this._processor.parseAsync(null, channel.URI);
-      this._processor.onStartRequest(aRequest, aContext);
-    }
-    catch (ex) {
-      Components.utils.reportError("Livemark Service: feed processor received an invalid channel for " + channel.URI.spec);
-      this.abort(ex);
-    }
-  },
-
-  onStopRequest(aRequest, aContext, aStatus) {
-    if (!Components.isSuccessCode(aStatus)) {
-      this.abort();
-      return;
-    }
-
-    // Set an expiration on the livemark, to reloading the data in future.
-    try {
-      if (this._processor) {
-        this._processor.onStopRequest(aRequest, aContext, aStatus);
-      }
-
-      // Calculate a new ttl
-      let channel = aRequest.QueryInterface(Ci.nsICachingChannel);
-      if (channel) {
-        let entryInfo = channel.cacheToken.QueryInterface(Ci.nsICacheEntry);
-        if (entryInfo) {
-          // nsICacheEntry returns value as seconds.
-          let expireTime = entryInfo.expirationTime * 1000;
-          let nowTime = Date.now();
-          // Note, expireTime can be 0, see bug 383538.
-          if (expireTime > nowTime) {
-            this._setResourceTTL(Math.max((expireTime - nowTime),
-                                          EXPIRE_TIME_MS));
-            return;
-          }
-        }
-      }
-      this._setResourceTTL(EXPIRE_TIME_MS);
-    }
-    catch (ex) {
-      this.abort(ex);
-    }
-    finally {
-      if (this._livemark.status == Ci.mozILivemark.STATUS_LOADING) {
-        this._livemark.status = Ci.mozILivemark.STATUS_READY;
-      }
-      this._livemark.locked = false;
-      this._livemark.loadGroup = null;
-    }
-  },
-
-  _setResourceTTL(aMilliseconds) {
-    this._livemark.expireTime = Date.now() + aMilliseconds;
-  },
-
-  // nsIInterfaceRequestor
-  getInterface(aIID) {
-    return this.QueryInterface(aIID);
-  },
-
-  // nsISupports
-  QueryInterface: XPCOMUtils.generateQI([
-    Ci.nsIFeedResultListener
-  , Ci.nsIStreamListener
-  , Ci.nsIRequestObserver
-  , Ci.nsIInterfaceRequestor
-  ])
-}
-
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LivemarkService]);