summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/nsLivemarkService.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/places/nsLivemarkService.js')
-rw-r--r--toolkit/components/places/nsLivemarkService.js891
1 files changed, 891 insertions, 0 deletions
diff --git a/toolkit/components/places/nsLivemarkService.js b/toolkit/components/places/nsLivemarkService.js
new file mode 100644
index 000000000..eeca7e139
--- /dev/null
+++ b/toolkit/components/places/nsLivemarkService.js
@@ -0,0 +1,891 @@
+/* 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;
+
+// Modules and services.
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.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) {
+ return date * 1000;
+}
+
+/**
+ * Convert a PRTime to a Date object.
+ *
+ * @param time
+ * microseconds from the epoch.
+ * @return a Date object or undefined if time was not defined.
+ */
+function toDate(time) {
+ return time ? new Date(parseInt(time / 1000)) : undefined;
+}
+
+// LivemarkService
+
+function LivemarkService() {
+ // Cleanup on shutdown.
+ 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,
+
+ _reloading: false,
+ _startReloadTimer(livemarksMap, forceUpdate, reloaded) {
+ if (this._reloadTimer) {
+ this._reloadTimer.cancel();
+ }
+ else {
+ this._reloadTimer = Cc["@mozilla.org/timer;1"]
+ .createInstance(Ci.nsITimer);
+ }
+
+ this._reloading = true;
+ this._reloadTimer.initWithCallback(() => {
+ // Find first livemark to be reloaded.
+ for (let [ guid, livemark ] of livemarksMap) {
+ if (!reloaded.has(guid)) {
+ reloaded.add(guid);
+ livemark.reload(forceUpdate);
+ this._startReloadTimer(livemarksMap, forceUpdate, reloaded);
+ return;
+ }
+ }
+ // All livemarks have been reloaded.
+ this._reloading = false;
+ }, RELOAD_DELAY_MS, Ci.nsITimer.TYPE_ONE_SHOT);
+ },
+
+ // nsIObserver
+
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == PlacesUtils.TOPIC_SHUTDOWN) {
+ if (this._reloadTimer) {
+ this._reloading = false;
+ this._reloadTimer.cancel();
+ delete this._reloadTimer;
+ }
+
+ // Stop any ongoing network fetch.
+ this._promiseLivemarksMap().then(livemarksMap => {
+ for (let livemark of livemarksMap.values()) {
+ livemark.terminate();
+ }
+ });
+ }
+ },
+
+ // mozIAsyncLivemarks
+
+ addLivemark(aLivemarkInfo) {
+ if (!aLivemarkInfo) {
+ throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
+ }
+ let hasParentId = "parentId" in aLivemarkInfo;
+ let hasParentGuid = "parentGuid" in aLivemarkInfo;
+ let hasIndex = "index" in aLivemarkInfo;
+ // Must provide at least non-null parent guid/id, index and feedURI.
+ if ((!hasParentId && !hasParentGuid) ||
+ (hasParentId && aLivemarkInfo.parentId < 1) ||
+ (hasParentGuid &&!/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.parentGuid)) ||
+ (hasIndex && aLivemarkInfo.index < Ci.nsINavBookmarksService.DEFAULT_INDEX) ||
+ !(aLivemarkInfo.feedURI instanceof Ci.nsIURI) ||
+ (aLivemarkInfo.siteURI && !(aLivemarkInfo.siteURI instanceof Ci.nsIURI)) ||
+ (aLivemarkInfo.guid && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid))) {
+ throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ return Task.spawn(function* () {
+ if (!aLivemarkInfo.parentGuid)
+ aLivemarkInfo.parentGuid = yield PlacesUtils.promiseItemGuid(aLivemarkInfo.parentId);
+
+ let livemarksMap = yield this._promiseLivemarksMap();
+
+ // Disallow adding a livemark inside another livemark.
+ if (livemarksMap.has(aLivemarkInfo.parentGuid)) {
+ throw new Components.Exception("Cannot create a livemark inside a livemark", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ // Create a new livemark.
+ let folder = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: aLivemarkInfo.parentGuid,
+ title: aLivemarkInfo.title,
+ index: aLivemarkInfo.index,
+ guid: aLivemarkInfo.guid,
+ dateAdded: toDate(aLivemarkInfo.dateAdded) || toDate(aLivemarkInfo.lastModified),
+ source: aLivemarkInfo.source,
+ });
+
+ // Set feed and site URI annotations.
+ let id = yield PlacesUtils.promiseItemId(folder.guid);
+
+ // Create the internal Livemark object.
+ let livemark = new Livemark({ id
+ , title: folder.title
+ , parentGuid: folder.parentGuid
+ , parentId: yield PlacesUtils.promiseItemId(folder.parentGuid)
+ , index: folder.index
+ , feedURI: aLivemarkInfo.feedURI
+ , siteURI: aLivemarkInfo.siteURI
+ , guid: folder.guid
+ , dateAdded: toPRTime(folder.dateAdded)
+ , lastModified: toPRTime(folder.lastModified)
+ });
+
+ livemark.writeFeedURI(aLivemarkInfo.feedURI, aLivemarkInfo.source);
+ if (aLivemarkInfo.siteURI) {
+ livemark.writeSiteURI(aLivemarkInfo.siteURI, aLivemarkInfo.source);
+ }
+
+ if (aLivemarkInfo.lastModified) {
+ yield PlacesUtils.bookmarks.update({ guid: folder.guid,
+ lastModified: toDate(aLivemarkInfo.lastModified),
+ source: aLivemarkInfo.source });
+ livemark.lastModified = aLivemarkInfo.lastModified;
+ }
+
+ livemarksMap.set(folder.guid, livemark);
+
+ return livemark;
+ }.bind(this));
+ },
+
+ removeLivemark(aLivemarkInfo) {
+ if (!aLivemarkInfo) {
+ throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
+ }
+ // Accept either a guid or an id.
+ let hasGuid = "guid" in aLivemarkInfo;
+ let hasId = "id" in aLivemarkInfo;
+ if ((hasGuid && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) ||
+ (hasId && aLivemarkInfo.id < 1) ||
+ (!hasId && !hasGuid)) {
+ throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ return Task.spawn(function* () {
+ if (!aLivemarkInfo.guid)
+ aLivemarkInfo.guid = yield PlacesUtils.promiseItemGuid(aLivemarkInfo.id);
+
+ let livemarksMap = yield this._promiseLivemarksMap();
+ if (!livemarksMap.has(aLivemarkInfo.guid))
+ throw new Components.Exception("Invalid livemark", Cr.NS_ERROR_INVALID_ARG);
+
+ yield PlacesUtils.bookmarks.remove(aLivemarkInfo.guid,
+ { source: aLivemarkInfo.source });
+ }.bind(this));
+ },
+
+ reloadLivemarks(aForceUpdate) {
+ // Check if there's a currently running reload, to save some useless work.
+ let notWorthRestarting =
+ this._forceUpdate || // We're already forceUpdating.
+ !aForceUpdate; // The caller didn't request a forced update.
+ if (this._reloading && notWorthRestarting) {
+ // Ignore this call.
+ return;
+ }
+
+ this._promiseLivemarksMap().then(livemarksMap => {
+ this._forceUpdate = !!aForceUpdate;
+ // Livemarks reloads happen on a timer for performance reasons.
+ this._startReloadTimer(livemarksMap, this._forceUpdate, new Set());
+ });
+ },
+
+ getLivemark(aLivemarkInfo) {
+ if (!aLivemarkInfo) {
+ throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
+ }
+ // Accept either a guid or an id.
+ let hasGuid = "guid" in aLivemarkInfo;
+ let hasId = "id" in aLivemarkInfo;
+ if ((hasGuid && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) ||
+ (hasId && aLivemarkInfo.id < 1) ||
+ (!hasId && !hasGuid)) {
+ throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ return Task.spawn(function*() {
+ if (!aLivemarkInfo.guid)
+ aLivemarkInfo.guid = yield PlacesUtils.promiseItemGuid(aLivemarkInfo.id);
+
+ let livemarksMap = yield this._promiseLivemarksMap();
+ if (!livemarksMap.has(aLivemarkInfo.guid))
+ throw new Components.Exception("Invalid livemark", Cr.NS_ERROR_INVALID_ARG);
+
+ return livemarksMap.get(aLivemarkInfo.guid);
+ }.bind(this));
+ },
+
+ // nsINavBookmarkObserver
+
+ onBeginUpdateBatch() {},
+ onEndUpdateBatch() {},
+ onItemVisited() {},
+ onItemAdded() {},
+
+ onItemChanged(id, property, isAnno, value, lastModified, itemType, parentId,
+ guid, parentGuid) {
+ if (itemType != Ci.nsINavBookmarksService.TYPE_FOLDER)
+ return;
+
+ this._promiseLivemarksMap().then(livemarksMap => {
+ if (livemarksMap.has(guid)) {
+ let livemark = livemarksMap.get(guid);
+ if (property == "title") {
+ livemark.title = value;
+ }
+ livemark.lastModified = lastModified;
+ }
+ });
+ },
+
+ onItemMoved(id, parentId, oldIndex, newParentId, newIndex, itemType, guid,
+ oldParentGuid, newParentGuid) {
+ if (itemType != Ci.nsINavBookmarksService.TYPE_FOLDER)
+ return;
+
+ this._promiseLivemarksMap().then(livemarksMap => {
+ if (livemarksMap.has(guid)) {
+ let livemark = livemarksMap.get(guid);
+ livemark.parentId = newParentId;
+ livemark.parentGuid = newParentGuid;
+ livemark.index = newIndex;
+ }
+ });
+ },
+
+ onItemRemoved(id, parentId, index, itemType, uri, guid, parentGuid) {
+ if (itemType != Ci.nsINavBookmarksService.TYPE_FOLDER)
+ return;
+
+ this._promiseLivemarksMap().then(livemarksMap => {
+ if (livemarksMap.has(guid)) {
+ let livemark = livemarksMap.get(guid);
+ livemark.terminate();
+ livemarksMap.delete(guid);
+ }
+ });
+ },
+
+ // nsINavHistoryObserver
+
+ onPageChanged() {},
+ onTitleChanged() {},
+ onDeleteVisits() {},
+
+ onClearHistory() {
+ this._promiseLivemarksMap().then(livemarksMap => {
+ for (let livemark of livemarksMap.values()) {
+ livemark.updateURIVisitedStatus(null, false);
+ }
+ });
+ },
+
+ onDeleteURI(aURI) {
+ this._promiseLivemarksMap().then(livemarksMap => {
+ for (let livemark of livemarksMap.values()) {
+ livemark.updateURIVisitedStatus(aURI, false);
+ }
+ });
+ },
+
+ onVisit(aURI) {
+ this._promiseLivemarksMap().then(livemarksMap => {
+ for (let livemark of livemarksMap.values()) {
+ livemark.updateURIVisitedStatus(aURI, true);
+ }
+ });
+ },
+
+ // nsISupports
+
+ classID: Components.ID("{dca61eb5-c7cd-4df1-b0fb-d0722baba251}"),
+
+ _xpcom_factory: XPCOMUtils.generateSingletonFactory(LivemarkService),
+
+ QueryInterface: XPCOMUtils.generateQI([
+ 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, aSource) {
+ PlacesUtils.annotations
+ .setItemAnnotation(this.id, PlacesUtils.LMANNO_FEEDURI,
+ aFeedURI.spec,
+ 0, PlacesUtils.annotations.EXPIRE_NEVER,
+ aSource);
+ this.feedURI = aFeedURI;
+ },
+
+ writeSiteURI(aSiteURI, aSource) {
+ if (!aSiteURI) {
+ PlacesUtils.annotations.removeItemAnnotation(this.id,
+ PlacesUtils.LMANNO_SITEURI,
+ aSource)
+ 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,
+ aSource);
+ 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]);