diff options
Diffstat (limited to 'toolkit/components/places/nsLivemarkService.js')
-rw-r--r-- | toolkit/components/places/nsLivemarkService.js | 891 |
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]); |