michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: const Cu = Components.utils; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Modules michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", michael@0: "resource://gre/modules/PlacesUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", michael@0: "resource://gre/modules/NetUtil.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Promise", michael@0: "resource://gre/modules/Promise.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Deprecated", michael@0: "resource://gre/modules/Deprecated.jsm"); michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Services michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "secMan", michael@0: "@mozilla.org/scriptsecuritymanager;1", michael@0: "nsIScriptSecurityManager"); michael@0: XPCOMUtils.defineLazyGetter(this, "asyncHistory", function () { michael@0: // Lazily add an history observer when it's actually needed. michael@0: PlacesUtils.history.addObserver(PlacesUtils.livemarks, true); michael@0: return Cc["@mozilla.org/browser/history;1"].getService(Ci.mozIAsyncHistory); michael@0: }); michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Constants michael@0: michael@0: // Security flags for checkLoadURIWithPrincipal. michael@0: const SEC_FLAGS = Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL; michael@0: michael@0: // Delay between reloads of consecute livemarks. michael@0: const RELOAD_DELAY_MS = 500; michael@0: // Expire livemarks after this time. michael@0: const EXPIRE_TIME_MS = 3600000; // 1 hour. michael@0: // Expire livemarks after this time on error. michael@0: const ONERROR_EXPIRE_TIME_MS = 300000; // 5 minutes. michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// LivemarkService michael@0: michael@0: function LivemarkService() michael@0: { michael@0: // Cleanup on shutdown. michael@0: Services.obs.addObserver(this, PlacesUtils.TOPIC_SHUTDOWN, true); michael@0: michael@0: // Observe bookmarks and history, but don't init the services just for that. michael@0: PlacesUtils.addLazyBookmarkObserver(this, true); michael@0: michael@0: // Asynchronously build the livemarks cache. michael@0: this._ensureAsynchronousCache(); michael@0: } michael@0: michael@0: LivemarkService.prototype = { michael@0: // Cache of Livemark objects, hashed by bookmarks folder ids. michael@0: _livemarks: {}, michael@0: // Hash associating guids to bookmarks folder ids. michael@0: _guids: {}, michael@0: michael@0: get _populateCacheSQL() michael@0: { michael@0: function getAnnoSQLFragment(aAnnoParam) { michael@0: return "SELECT a.content " michael@0: + "FROM moz_items_annos a " michael@0: + "JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id " michael@0: + "WHERE a.item_id = b.id " michael@0: + "AND n.name = " + aAnnoParam; michael@0: } michael@0: michael@0: return "SELECT b.id, b.title, b.parent, b.position, b.guid, b.lastModified, " michael@0: + "(" + getAnnoSQLFragment(":feedURI_anno") + ") AS feedURI, " michael@0: + "(" + getAnnoSQLFragment(":siteURI_anno") + ") AS siteURI " michael@0: + "FROM moz_bookmarks b " michael@0: + "JOIN moz_items_annos a ON a.item_id = b.id " michael@0: + "JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id " michael@0: + "WHERE b.type = :folder_type " michael@0: + "AND n.name = :feedURI_anno "; michael@0: }, michael@0: michael@0: _ensureAsynchronousCache: function LS__ensureAsynchronousCache() michael@0: { michael@0: let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) michael@0: .DBConnection; michael@0: let stmt = db.createAsyncStatement(this._populateCacheSQL); michael@0: stmt.params.folder_type = Ci.nsINavBookmarksService.TYPE_FOLDER; michael@0: stmt.params.feedURI_anno = PlacesUtils.LMANNO_FEEDURI; michael@0: stmt.params.siteURI_anno = PlacesUtils.LMANNO_SITEURI; michael@0: michael@0: let livemarkSvc = this; michael@0: this._pendingStmt = stmt.executeAsync({ michael@0: handleResult: function LS_handleResult(aResults) michael@0: { michael@0: for (let row = aResults.getNextRow(); row; row = aResults.getNextRow()) { michael@0: let id = row.getResultByName("id"); michael@0: let siteURL = row.getResultByName("siteURI"); michael@0: let guid = row.getResultByName("guid"); michael@0: livemarkSvc._livemarks[id] = michael@0: new Livemark({ id: id, michael@0: guid: guid, michael@0: title: row.getResultByName("title"), michael@0: parentId: row.getResultByName("parent"), michael@0: index: row.getResultByName("position"), michael@0: lastModified: row.getResultByName("lastModified"), michael@0: feedURI: NetUtil.newURI(row.getResultByName("feedURI")), michael@0: siteURI: siteURL ? NetUtil.newURI(siteURL) : null, michael@0: }); michael@0: livemarkSvc._guids[guid] = id; michael@0: } michael@0: }, michael@0: handleError: function LS_handleError(aErr) michael@0: { michael@0: Cu.reportError("AsyncStmt error (" + aErr.result + "): '" + aErr.message); michael@0: }, michael@0: handleCompletion: function LS_handleCompletion() { michael@0: livemarkSvc._pendingStmt = null; michael@0: } michael@0: }); michael@0: stmt.finalize(); michael@0: }, michael@0: michael@0: _onCacheReady: function LS__onCacheReady(aCallback) michael@0: { michael@0: if (this._pendingStmt) { michael@0: // The cache is still being populated, so enqueue the job to the Storage michael@0: // async thread. Ideally this should just dispatch a runnable to it, michael@0: // that would call back on the main thread, but bug 608142 made that michael@0: // impossible. Thus just enqueue the cheapest query possible. michael@0: let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) michael@0: .DBConnection; michael@0: let stmt = db.createAsyncStatement("PRAGMA encoding"); michael@0: stmt.executeAsync({ michael@0: handleError: function () {}, michael@0: handleResult: function () {}, michael@0: handleCompletion: function ETAT_handleCompletion() michael@0: { michael@0: aCallback(); michael@0: } michael@0: }); michael@0: stmt.finalize(); michael@0: } michael@0: else { michael@0: // The callbacks should always be enqueued per the interface. michael@0: // Just enque on the main thread. michael@0: Services.tm.mainThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL); michael@0: } michael@0: }, michael@0: michael@0: _reloading: false, michael@0: _startReloadTimer: function LS__startReloadTimer() michael@0: { michael@0: if (this._reloadTimer) { michael@0: this._reloadTimer.cancel(); michael@0: } michael@0: else { michael@0: this._reloadTimer = Cc["@mozilla.org/timer;1"] michael@0: .createInstance(Ci.nsITimer); michael@0: } michael@0: this._reloading = true; michael@0: this._reloadTimer.initWithCallback(this._reloadNextLivemark.bind(this), michael@0: RELOAD_DELAY_MS, michael@0: Ci.nsITimer.TYPE_ONE_SHOT); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsIObserver michael@0: michael@0: observe: function LS_observe(aSubject, aTopic, aData) michael@0: { michael@0: if (aTopic == PlacesUtils.TOPIC_SHUTDOWN) { michael@0: if (this._pendingStmt) { michael@0: this._pendingStmt.cancel(); michael@0: this._pendingStmt = null; michael@0: // Initialization never finished, so just bail out. michael@0: return; michael@0: } michael@0: michael@0: if (this._reloadTimer) { michael@0: this._reloading = false; michael@0: this._reloadTimer.cancel(); michael@0: delete this._reloadTimer; michael@0: } michael@0: michael@0: // Stop any ongoing update. michael@0: for each (let livemark in this._livemarks) { michael@0: livemark.terminate(); michael@0: } michael@0: this._livemarks = {}; michael@0: } michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// mozIAsyncLivemarks michael@0: michael@0: addLivemark: function LS_addLivemark(aLivemarkInfo, michael@0: aLivemarkCallback) michael@0: { michael@0: // Must provide at least non-null parentId, index and feedURI. michael@0: if (!aLivemarkInfo || michael@0: ("parentId" in aLivemarkInfo && aLivemarkInfo.parentId < 1) || michael@0: !("index" in aLivemarkInfo) || aLivemarkInfo.index < Ci.nsINavBookmarksService.DEFAULT_INDEX || michael@0: !(aLivemarkInfo.feedURI instanceof Ci.nsIURI) || michael@0: (aLivemarkInfo.siteURI && !(aLivemarkInfo.siteURI instanceof Ci.nsIURI)) || michael@0: (aLivemarkInfo.guid && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid))) { michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: } michael@0: michael@0: if (aLivemarkCallback) { michael@0: Deprecated.warning("Passing a callback to Livermarks methods is deprecated. " + michael@0: "Please use the returned promise instead.", michael@0: "https://developer.mozilla.org/docs/Mozilla/JavaScript_code_modules/Promise.jsm"); michael@0: } michael@0: michael@0: // The addition is done synchronously due to the fact importExport service michael@0: // and JSON backups require that. The notification is async though. michael@0: // Once bookmarks are async, this may be properly fixed. michael@0: let deferred = Promise.defer(); michael@0: let addLivemarkEx = null; michael@0: let livemark = null; michael@0: try { michael@0: // Disallow adding a livemark inside another livemark. michael@0: if (aLivemarkInfo.parentId in this._livemarks) { michael@0: throw new Components.Exception("", Cr.NS_ERROR_INVALID_ARG); michael@0: } michael@0: michael@0: // Don't pass unexpected input data to the livemark constructor. michael@0: livemark = new Livemark({ title: aLivemarkInfo.title michael@0: , parentId: aLivemarkInfo.parentId michael@0: , index: aLivemarkInfo.index michael@0: , feedURI: aLivemarkInfo.feedURI michael@0: , siteURI: aLivemarkInfo.siteURI michael@0: , guid: aLivemarkInfo.guid michael@0: , lastModified: aLivemarkInfo.lastModified michael@0: }); michael@0: if (this._itemAdded && this._itemAdded.id == livemark.id) { michael@0: livemark.index = this._itemAdded.index; michael@0: livemark.guid = this._itemAdded.guid; michael@0: if (!aLivemarkInfo.lastModified) { michael@0: livemark.lastModified = this._itemAdded.lastModified; michael@0: } michael@0: } michael@0: michael@0: // Updating the cache even if it has not yet been populated doesn't michael@0: // matter since it will just be overwritten. michael@0: this._livemarks[livemark.id] = livemark; michael@0: this._guids[livemark.guid] = livemark.id; michael@0: } michael@0: catch (ex) { michael@0: addLivemarkEx = ex; michael@0: livemark = null; michael@0: } michael@0: finally { michael@0: this._onCacheReady( () => { michael@0: if (addLivemarkEx) { michael@0: if (aLivemarkCallback) { michael@0: try { michael@0: aLivemarkCallback.onCompletion(addLivemarkEx.result, livemark); michael@0: } michael@0: catch(ex2) { } michael@0: } else { michael@0: deferred.reject(addLivemarkEx); michael@0: } michael@0: } michael@0: else { michael@0: if (aLivemarkCallback) { michael@0: try { michael@0: aLivemarkCallback.onCompletion(Cr.NS_OK, livemark); michael@0: } michael@0: catch(ex2) { } michael@0: } else { michael@0: deferred.resolve(livemark); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: return aLivemarkCallback ? null : deferred.promise; michael@0: }, michael@0: michael@0: removeLivemark: function LS_removeLivemark(aLivemarkInfo, aLivemarkCallback) michael@0: { michael@0: if (!aLivemarkInfo) { michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: } michael@0: michael@0: // Accept either a guid or an id. michael@0: let id = aLivemarkInfo.guid || aLivemarkInfo.id; michael@0: if (("guid" in aLivemarkInfo && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) || michael@0: ("id" in aLivemarkInfo && aLivemarkInfo.id < 1) || michael@0: !id) { michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: } michael@0: michael@0: if (aLivemarkCallback) { michael@0: Deprecated.warning("Passing a callback to Livermarks methods is deprecated. " + michael@0: "Please use the returned promise instead.", michael@0: "https://developer.mozilla.org/docs/Mozilla/JavaScript_code_modules/Promise.jsm"); michael@0: } michael@0: michael@0: // Convert the guid to an id. michael@0: if (id in this._guids) { michael@0: id = this._guids[id]; michael@0: } michael@0: michael@0: let deferred = Promise.defer(); michael@0: let removeLivemarkEx = null; michael@0: try { michael@0: if (!(id in this._livemarks)) { michael@0: throw new Components.Exception("", Cr.NS_ERROR_INVALID_ARG); michael@0: } michael@0: this._livemarks[id].remove(); michael@0: } michael@0: catch (ex) { michael@0: removeLivemarkEx = ex; michael@0: } michael@0: finally { michael@0: this._onCacheReady( () => { michael@0: if (removeLivemarkEx) { michael@0: if (aLivemarkCallback) { michael@0: try { michael@0: aLivemarkCallback.onCompletion(removeLivemarkEx.result, null); michael@0: } michael@0: catch(ex2) { } michael@0: } else { michael@0: deferred.reject(removeLivemarkEx); michael@0: } michael@0: } michael@0: else { michael@0: if (aLivemarkCallback) { michael@0: try { michael@0: aLivemarkCallback.onCompletion(Cr.NS_OK, null); michael@0: } michael@0: catch(ex2) { } michael@0: } else { michael@0: deferred.resolve(); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: return aLivemarkCallback ? null : deferred.promise; michael@0: }, michael@0: michael@0: _reloaded: [], michael@0: _reloadNextLivemark: function LS__reloadNextLivemark() michael@0: { michael@0: this._reloading = false; michael@0: // Find first livemark to be reloaded. michael@0: for (let id in this._livemarks) { michael@0: if (this._reloaded.indexOf(id) == -1) { michael@0: this._reloaded.push(id); michael@0: this._livemarks[id].reload(this._forceUpdate); michael@0: this._startReloadTimer(); michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: reloadLivemarks: function LS_reloadLivemarks(aForceUpdate) michael@0: { michael@0: // Check if there's a currently running reload, to save some useless work. michael@0: let notWorthRestarting = michael@0: this._forceUpdate || // We're already forceUpdating. michael@0: !aForceUpdate; // The caller didn't request a forced update. michael@0: if (this._reloading && notWorthRestarting) { michael@0: // Ignore this call. michael@0: return; michael@0: } michael@0: michael@0: this._onCacheReady( () => { michael@0: this._forceUpdate = !!aForceUpdate; michael@0: this._reloaded = []; michael@0: // Livemarks reloads happen on a timer, and are delayed for performance michael@0: // reasons. michael@0: this._startReloadTimer(); michael@0: }); michael@0: }, michael@0: michael@0: getLivemark: function LS_getLivemark(aLivemarkInfo, aLivemarkCallback) michael@0: { michael@0: if (!aLivemarkInfo) { michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: } michael@0: // Accept either a guid or an id. michael@0: let id = aLivemarkInfo.guid || aLivemarkInfo.id; michael@0: if (("guid" in aLivemarkInfo && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) || michael@0: ("id" in aLivemarkInfo && aLivemarkInfo.id < 1) || michael@0: !id) { michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: } michael@0: michael@0: if (aLivemarkCallback) { michael@0: Deprecated.warning("Passing a callback to Livermarks methods is deprecated. " + michael@0: "Please use the returned promise instead.", michael@0: "https://developer.mozilla.org/docs/Mozilla/JavaScript_code_modules/Promise.jsm"); michael@0: } michael@0: michael@0: let deferred = Promise.defer(); michael@0: this._onCacheReady( () => { michael@0: // Convert the guid to an id. michael@0: if (id in this._guids) { michael@0: id = this._guids[id]; michael@0: } michael@0: if (id in this._livemarks) { michael@0: if (aLivemarkCallback) { michael@0: try { michael@0: aLivemarkCallback.onCompletion(Cr.NS_OK, this._livemarks[id]); michael@0: } catch (ex) {} michael@0: } else { michael@0: deferred.resolve(this._livemarks[id]); michael@0: } michael@0: } michael@0: else { michael@0: if (aLivemarkCallback) { michael@0: try { michael@0: aLivemarkCallback.onCompletion(Cr.NS_ERROR_INVALID_ARG, null); michael@0: } catch (ex) { } michael@0: } else { michael@0: deferred.reject(Components.Exception("", Cr.NS_ERROR_INVALID_ARG)); michael@0: } michael@0: } michael@0: }); michael@0: michael@0: return aLivemarkCallback ? null : deferred.promise; michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsINavBookmarkObserver michael@0: michael@0: onBeginUpdateBatch: function () {}, michael@0: onEndUpdateBatch: function () {}, michael@0: onItemVisited: function () {}, michael@0: michael@0: _itemAdded: null, michael@0: onItemAdded: function LS_onItemAdded(aItemId, aParentId, aIndex, aItemType, michael@0: aURI, aTitle, aDateAdded, aGUID) michael@0: { michael@0: if (aItemType == Ci.nsINavBookmarksService.TYPE_FOLDER) { michael@0: this._itemAdded = { id: aItemId michael@0: , guid: aGUID michael@0: , index: aIndex michael@0: , lastModified: aDateAdded michael@0: }; michael@0: } michael@0: }, michael@0: michael@0: onItemChanged: function LS_onItemChanged(aItemId, aProperty, aIsAnno, aValue, michael@0: aLastModified, aItemType) michael@0: { michael@0: if (aItemType == Ci.nsINavBookmarksService.TYPE_FOLDER) { michael@0: if (this._itemAdded && this._itemAdded.id == aItemId) { michael@0: this._itemAdded.lastModified = aLastModified; michael@0: } michael@0: if (aItemId in this._livemarks) { michael@0: if (aProperty == "title") { michael@0: this._livemarks[aItemId].title = aValue; michael@0: } michael@0: this._livemarks[aItemId].lastModified = aLastModified; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: onItemMoved: function LS_onItemMoved(aItemId, aOldParentId, aOldIndex, michael@0: aNewParentId, aNewIndex, aItemType) michael@0: { michael@0: if (aItemType == Ci.nsINavBookmarksService.TYPE_FOLDER && michael@0: aItemId in this._livemarks) { michael@0: this._livemarks[aItemId].parentId = aNewParentId; michael@0: this._livemarks[aItemId].index = aNewIndex; michael@0: } michael@0: }, michael@0: michael@0: onItemRemoved: function LS_onItemRemoved(aItemId, aParentId, aIndex, michael@0: aItemType, aURI, aGUID) michael@0: { michael@0: if (aItemType == Ci.nsINavBookmarksService.TYPE_FOLDER && michael@0: aItemId in this._livemarks) { michael@0: this._livemarks[aItemId].terminate(); michael@0: delete this._livemarks[aItemId]; michael@0: delete this._guids[aGUID]; michael@0: } michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsINavHistoryObserver michael@0: michael@0: onBeginUpdateBatch: function () {}, michael@0: onEndUpdateBatch: function () {}, michael@0: onPageChanged: function () {}, michael@0: onTitleChanged: function () {}, michael@0: onDeleteVisits: function () {}, michael@0: onClearHistory: function () {}, michael@0: michael@0: onDeleteURI: function PS_onDeleteURI(aURI) { michael@0: for each (let livemark in this._livemarks) { michael@0: livemark.updateURIVisitedStatus(aURI, false); michael@0: } michael@0: }, michael@0: michael@0: onVisit: function PS_onVisit(aURI) { michael@0: for each (let livemark in this._livemarks) { michael@0: livemark.updateURIVisitedStatus(aURI, true); michael@0: } michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsISupports michael@0: michael@0: classID: Components.ID("{dca61eb5-c7cd-4df1-b0fb-d0722baba251}"), michael@0: michael@0: _xpcom_factory: XPCOMUtils.generateSingletonFactory(LivemarkService), michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.mozIAsyncLivemarks michael@0: , Ci.nsINavBookmarkObserver michael@0: , Ci.nsINavHistoryObserver michael@0: , Ci.nsIObserver michael@0: , Ci.nsISupportsWeakReference michael@0: ]) michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Livemark michael@0: michael@0: /** michael@0: * Object used internally to represent a livemark. michael@0: * michael@0: * @param aLivemarkInfo michael@0: * Object containing information on the livemark. If the livemark is michael@0: * not included in the object, a new livemark will be created. michael@0: * michael@0: * @note terminate() must be invoked before getting rid of this object. michael@0: */ michael@0: function Livemark(aLivemarkInfo) michael@0: { michael@0: this.title = aLivemarkInfo.title; michael@0: this.parentId = aLivemarkInfo.parentId; michael@0: this.index = aLivemarkInfo.index; michael@0: michael@0: this._status = Ci.mozILivemark.STATUS_READY; michael@0: michael@0: // Hash of resultObservers, hashed by container. michael@0: this._resultObservers = new Map(); michael@0: // This keeps a list of the containers used as keys in the map, since michael@0: // it's not iterable. In future may use an iterable Map. michael@0: this._resultObserversList = []; michael@0: michael@0: // Sorted array of objects representing livemark children in the form michael@0: // { uri, title, visited }. michael@0: this._children = []; michael@0: michael@0: // Keeps a separate array of nodes for each requesting container, hashed by michael@0: // the container itself. michael@0: this._nodes = new Map(); michael@0: michael@0: this._guid = ""; michael@0: this._lastModified = 0; michael@0: michael@0: this.loadGroup = null; michael@0: this.feedURI = null; michael@0: this.siteURI = null; michael@0: this.expireTime = 0; michael@0: michael@0: if (aLivemarkInfo.id) { michael@0: // This request comes from the cache. michael@0: this.id = aLivemarkInfo.id; michael@0: this.guid = aLivemarkInfo.guid; michael@0: this.feedURI = aLivemarkInfo.feedURI; michael@0: this.siteURI = aLivemarkInfo.siteURI; michael@0: this.lastModified = aLivemarkInfo.lastModified; michael@0: } michael@0: else { michael@0: // Create a new livemark. michael@0: this.id = PlacesUtils.bookmarks.createFolder(aLivemarkInfo.parentId, michael@0: aLivemarkInfo.title, michael@0: aLivemarkInfo.index, michael@0: aLivemarkInfo.guid); michael@0: PlacesUtils.bookmarks.setFolderReadonly(this.id, true); michael@0: this.writeFeedURI(aLivemarkInfo.feedURI); michael@0: if (aLivemarkInfo.siteURI) { michael@0: this.writeSiteURI(aLivemarkInfo.siteURI); michael@0: } michael@0: // Last modified time must be the last change. michael@0: if (aLivemarkInfo.lastModified) { michael@0: this.lastModified = aLivemarkInfo.lastModified; michael@0: PlacesUtils.bookmarks.setItemLastModified(this.id, this.lastModified); michael@0: } michael@0: } michael@0: } michael@0: michael@0: Livemark.prototype = { michael@0: get status() this._status, michael@0: set status(val) { michael@0: if (this._status != val) { michael@0: this._status = val; michael@0: this._invalidateRegisteredContainers(); michael@0: } michael@0: return this._status; michael@0: }, michael@0: michael@0: /** michael@0: * Sets an annotation on the bookmarks folder id representing the livemark. michael@0: * michael@0: * @param aAnnoName michael@0: * Name of the annotation. michael@0: * @param aValue michael@0: * Value of the annotation. michael@0: * @return The annotation value. michael@0: * @throws If the folder is invalid. michael@0: */ michael@0: _setAnno: function LM__setAnno(aAnnoName, aValue) michael@0: { michael@0: PlacesUtils.annotations michael@0: .setItemAnnotation(this.id, aAnnoName, aValue, 0, michael@0: PlacesUtils.annotations.EXPIRE_NEVER); michael@0: }, michael@0: michael@0: writeFeedURI: function LM_writeFeedURI(aFeedURI) michael@0: { michael@0: this._setAnno(PlacesUtils.LMANNO_FEEDURI, aFeedURI.spec); michael@0: this.feedURI = aFeedURI; michael@0: }, michael@0: michael@0: writeSiteURI: function LM_writeSiteURI(aSiteURI) michael@0: { michael@0: if (!aSiteURI) { michael@0: PlacesUtils.annotations.removeItemAnnotation(this.id, michael@0: PlacesUtils.LMANNO_SITEURI) michael@0: this.siteURI = null; michael@0: return; michael@0: } michael@0: michael@0: // Security check the site URI against the feed URI principal. michael@0: let feedPrincipal = secMan.getSimpleCodebasePrincipal(this.feedURI); michael@0: try { michael@0: secMan.checkLoadURIWithPrincipal(feedPrincipal, aSiteURI, SEC_FLAGS); michael@0: } michael@0: catch (ex) { michael@0: return; michael@0: } michael@0: michael@0: this._setAnno(PlacesUtils.LMANNO_SITEURI, aSiteURI.spec) michael@0: this.siteURI = aSiteURI; michael@0: }, michael@0: michael@0: set guid(aGUID) { michael@0: this._guid = aGUID; michael@0: return aGUID; michael@0: }, michael@0: get guid() this._guid, michael@0: michael@0: set lastModified(aLastModified) { michael@0: this._lastModified = aLastModified; michael@0: return aLastModified; michael@0: }, michael@0: get lastModified() this._lastModified, michael@0: michael@0: /** michael@0: * Tries to updates the livemark if needed. michael@0: * The update process is asynchronous. michael@0: * michael@0: * @param [optional] aForceUpdate michael@0: * If true will try to update the livemark even if its contents have michael@0: * not yet expired. michael@0: */ michael@0: updateChildren: function LM_updateChildren(aForceUpdate) michael@0: { michael@0: // Check if the livemark is already updating. michael@0: if (this.status == Ci.mozILivemark.STATUS_LOADING) michael@0: return; michael@0: michael@0: // Check the TTL/expiration on this, to check if there is no need to update michael@0: // this livemark. michael@0: if (!aForceUpdate && this.children.length && this.expireTime > Date.now()) michael@0: return; michael@0: michael@0: this.status = Ci.mozILivemark.STATUS_LOADING; michael@0: michael@0: // Setting the status notifies observers that may remove the livemark. michael@0: if (this._terminated) michael@0: return; michael@0: michael@0: try { michael@0: // Create a load group for the request. This will allow us to michael@0: // automatically keep track of redirects, so we can always michael@0: // cancel the channel. michael@0: let loadgroup = Cc["@mozilla.org/network/load-group;1"]. michael@0: createInstance(Ci.nsILoadGroup); michael@0: let channel = NetUtil.newChannel(this.feedURI.spec). michael@0: QueryInterface(Ci.nsIHttpChannel); michael@0: channel.loadGroup = loadgroup; michael@0: channel.loadFlags |= Ci.nsIRequest.LOAD_BACKGROUND | michael@0: Ci.nsIRequest.LOAD_BYPASS_CACHE; michael@0: channel.requestMethod = "GET"; michael@0: channel.setRequestHeader("X-Moz", "livebookmarks", false); michael@0: michael@0: // Stream the result to the feed parser with this listener michael@0: let listener = new LivemarkLoadListener(this); michael@0: channel.notificationCallbacks = listener; michael@0: channel.asyncOpen(listener, null); michael@0: michael@0: this.loadGroup = loadgroup; michael@0: } michael@0: catch (ex) { michael@0: this.status = Ci.mozILivemark.STATUS_FAILED; michael@0: } michael@0: }, michael@0: michael@0: reload: function LM_reload(aForceUpdate) michael@0: { michael@0: this.updateChildren(aForceUpdate); michael@0: }, michael@0: michael@0: remove: function LM_remove() { michael@0: PlacesUtils.bookmarks.removeItem(this.id); michael@0: }, michael@0: michael@0: get children() this._children, michael@0: set children(val) { michael@0: this._children = val; michael@0: michael@0: // Discard the previous cached nodes, new ones should be generated. michael@0: for (let i = 0; i < this._resultObserversList.length; i++) { michael@0: let container = this._resultObserversList[i]; michael@0: this._nodes.delete(container); michael@0: } michael@0: michael@0: // Update visited status for each entry. michael@0: for (let i = 0; i < this._children.length; i++) { michael@0: let child = this._children[i]; michael@0: asyncHistory.isURIVisited(child.uri, michael@0: (function(aURI, aIsVisited) { michael@0: this.updateURIVisitedStatus(aURI, aIsVisited); michael@0: }).bind(this)); michael@0: } michael@0: michael@0: return this._children; michael@0: }, michael@0: michael@0: _isURIVisited: function LM__isURIVisited(aURI) { michael@0: for (let i = 0; i < this.children.length; i++) { michael@0: if (this.children[i].uri.equals(aURI)) { michael@0: return this.children[i].visited; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: getNodesForContainer: function LM_getNodesForContainer(aContainerNode) michael@0: { michael@0: if (this._nodes.has(aContainerNode)) { michael@0: return this._nodes.get(aContainerNode); michael@0: } michael@0: michael@0: let livemark = this; michael@0: let nodes = []; michael@0: let now = Date.now() * 1000; michael@0: for (let i = 0; i < this._children.length; i++) { michael@0: let child = this._children[i]; michael@0: let node = { michael@0: // The QueryInterface is needed cause aContainerNode is a jsval. michael@0: // This is required to avoid issues with scriptable wrappers that would michael@0: // not allow the view to correctly set expandos. michael@0: get parent() michael@0: aContainerNode.QueryInterface(Ci.nsINavHistoryContainerResultNode), michael@0: get parentResult() this.parent.parentResult, michael@0: get uri() child.uri.spec, michael@0: get type() Ci.nsINavHistoryResultNode.RESULT_TYPE_URI, michael@0: get title() child.title, michael@0: get accessCount() michael@0: Number(livemark._isURIVisited(NetUtil.newURI(this.uri))), michael@0: get time() 0, michael@0: get icon() "", michael@0: get indentLevel() this.parent.indentLevel + 1, michael@0: get bookmarkIndex() -1, michael@0: get itemId() -1, michael@0: get dateAdded() now + i, michael@0: get lastModified() now + i, michael@0: get tags() michael@0: PlacesUtils.tagging.getTagsForURI(NetUtil.newURI(this.uri)).join(", "), michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryResultNode]) michael@0: }; michael@0: nodes.push(node); michael@0: } michael@0: this._nodes.set(aContainerNode, nodes); michael@0: return nodes; michael@0: }, michael@0: michael@0: registerForUpdates: function LM_registerForUpdates(aContainerNode, michael@0: aResultObserver) michael@0: { michael@0: this._resultObservers.set(aContainerNode, aResultObserver); michael@0: this._resultObserversList.push(aContainerNode); michael@0: }, michael@0: michael@0: unregisterForUpdates: function LM_unregisterForUpdates(aContainerNode) michael@0: { michael@0: this._resultObservers.delete(aContainerNode); michael@0: let index = this._resultObserversList.indexOf(aContainerNode); michael@0: this._resultObserversList.splice(index, 1); michael@0: michael@0: this._nodes.delete(aContainerNode); michael@0: }, michael@0: michael@0: _invalidateRegisteredContainers: function LM__invalidateRegisteredContainers() michael@0: { michael@0: for (let i = 0; i < this._resultObserversList.length; i++) { michael@0: let container = this._resultObserversList[i]; michael@0: let observer = this._resultObservers.get(container); michael@0: observer.invalidateContainer(container); michael@0: } michael@0: }, michael@0: michael@0: updateURIVisitedStatus: michael@0: function LM_updateURIVisitedStatus(aURI, aVisitedStatus) michael@0: { michael@0: for (let i = 0; i < this.children.length; i++) { michael@0: if (this.children[i].uri.equals(aURI)) { michael@0: this.children[i].visited = aVisitedStatus; michael@0: } michael@0: } michael@0: michael@0: for (let i = 0; i < this._resultObserversList.length; i++) { michael@0: let container = this._resultObserversList[i]; michael@0: let observer = this._resultObservers.get(container); michael@0: if (this._nodes.has(container)) { michael@0: let nodes = this._nodes.get(container); michael@0: for (let j = 0; j < nodes.length; j++) { michael@0: let node = nodes[j]; michael@0: if (node.uri == aURI.spec) { michael@0: Services.tm.mainThread.dispatch((function () { michael@0: observer.nodeHistoryDetailsChanged(node, 0, aVisitedStatus); michael@0: }).bind(this), Ci.nsIThread.DISPATCH_NORMAL); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Terminates the livemark entry, cancelling any ongoing load. michael@0: * Must be invoked before destroying the entry. michael@0: */ michael@0: terminate: function LM_terminate() michael@0: { michael@0: // Avoid handling any updateChildren request from now on. michael@0: this._terminated = true; michael@0: // Clear the list before aborting, since abort() would try to set the michael@0: // status and notify about it, but that's not really useful at this point. michael@0: this._resultObserversList = []; michael@0: this.abort(); michael@0: }, michael@0: michael@0: /** michael@0: * Aborts the livemark loading if needed. michael@0: */ michael@0: abort: function LM_abort() michael@0: { michael@0: this.status = Ci.mozILivemark.STATUS_FAILED; michael@0: if (this.loadGroup) { michael@0: this.loadGroup.cancel(Cr.NS_BINDING_ABORTED); michael@0: this.loadGroup = null; michael@0: } michael@0: }, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.mozILivemark michael@0: ]) michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// LivemarkLoadListener michael@0: michael@0: /** michael@0: * Object used internally to handle loading a livemark's contents. michael@0: * michael@0: * @param aLivemark michael@0: * The Livemark that is loading. michael@0: */ michael@0: function LivemarkLoadListener(aLivemark) michael@0: { michael@0: this._livemark = aLivemark; michael@0: this._processor = null; michael@0: this._isAborted = false; michael@0: this._ttl = EXPIRE_TIME_MS; michael@0: } michael@0: michael@0: LivemarkLoadListener.prototype = { michael@0: abort: function LLL_abort(aException) michael@0: { michael@0: if (!this._isAborted) { michael@0: this._isAborted = true; michael@0: this._livemark.abort(); michael@0: this._setResourceTTL(ONERROR_EXPIRE_TIME_MS); michael@0: } michael@0: }, michael@0: michael@0: // nsIFeedResultListener michael@0: handleResult: function LLL_handleResult(aResult) michael@0: { michael@0: if (this._isAborted) { michael@0: return; michael@0: } michael@0: michael@0: try { michael@0: // We need this to make sure the item links are safe michael@0: let feedPrincipal = michael@0: secMan.getSimpleCodebasePrincipal(this._livemark.feedURI); michael@0: michael@0: // Enforce well-formedness because the existing code does michael@0: if (!aResult || !aResult.doc || aResult.bozo) { michael@0: throw new Components.Exception("", Cr.NS_ERROR_FAILURE); michael@0: } michael@0: michael@0: let feed = aResult.doc.QueryInterface(Ci.nsIFeed); michael@0: let siteURI = this._livemark.siteURI; michael@0: if (feed.link && (!siteURI || !feed.link.equals(siteURI))) { michael@0: siteURI = feed.link; michael@0: this._livemark.writeSiteURI(siteURI); michael@0: } michael@0: michael@0: // Insert feed items. michael@0: let livemarkChildren = []; michael@0: for (let i = 0; i < feed.items.length; ++i) { michael@0: let entry = feed.items.queryElementAt(i, Ci.nsIFeedEntry); michael@0: let uri = entry.link || siteURI; michael@0: if (!uri) { michael@0: continue; michael@0: } michael@0: michael@0: try { michael@0: secMan.checkLoadURIWithPrincipal(feedPrincipal, uri, SEC_FLAGS); michael@0: } michael@0: catch(ex) { michael@0: continue; michael@0: } michael@0: michael@0: let title = entry.title ? entry.title.plainText() : ""; michael@0: livemarkChildren.push({ uri: uri, title: title, visited: false }); michael@0: } michael@0: michael@0: this._livemark.children = livemarkChildren; michael@0: } michael@0: catch (ex) { michael@0: this.abort(ex); michael@0: } michael@0: finally { michael@0: this._processor.listener = null; michael@0: this._processor = null; michael@0: } michael@0: }, michael@0: michael@0: onDataAvailable: function LLL_onDataAvailable(aRequest, aContext, michael@0: aInputStream, aSourceOffset, michael@0: aCount) michael@0: { michael@0: if (this._processor) { michael@0: this._processor.onDataAvailable(aRequest, aContext, aInputStream, michael@0: aSourceOffset, aCount); michael@0: } michael@0: }, michael@0: michael@0: onStartRequest: function LLL_onStartRequest(aRequest, aContext) michael@0: { michael@0: if (this._isAborted) { michael@0: throw Cr.NS_ERROR_UNEXPECTED; michael@0: } michael@0: michael@0: let channel = aRequest.QueryInterface(Ci.nsIChannel); michael@0: try { michael@0: // Parse feed data as it comes in michael@0: this._processor = Cc["@mozilla.org/feed-processor;1"]. michael@0: createInstance(Ci.nsIFeedProcessor); michael@0: this._processor.listener = this; michael@0: this._processor.parseAsync(null, channel.URI); michael@0: this._processor.onStartRequest(aRequest, aContext); michael@0: } michael@0: catch (ex) { michael@0: Components.utils.reportError("Livemark Service: feed processor received an invalid channel for " + channel.URI.spec); michael@0: this.abort(ex); michael@0: } michael@0: }, michael@0: michael@0: onStopRequest: function LLL_onStopRequest(aRequest, aContext, aStatus) michael@0: { michael@0: if (!Components.isSuccessCode(aStatus)) { michael@0: this.abort(); michael@0: return; michael@0: } michael@0: michael@0: // Set an expiration on the livemark, to reloading the data in future. michael@0: try { michael@0: if (this._processor) { michael@0: this._processor.onStopRequest(aRequest, aContext, aStatus); michael@0: } michael@0: michael@0: // Calculate a new ttl michael@0: let channel = aRequest.QueryInterface(Ci.nsICachingChannel); michael@0: if (channel) { michael@0: let entryInfo = channel.cacheToken.QueryInterface(Ci.nsICacheEntry); michael@0: if (entryInfo) { michael@0: // nsICacheEntry returns value as seconds. michael@0: let expireTime = entryInfo.expirationTime * 1000; michael@0: let nowTime = Date.now(); michael@0: // Note, expireTime can be 0, see bug 383538. michael@0: if (expireTime > nowTime) { michael@0: this._setResourceTTL(Math.max((expireTime - nowTime), michael@0: EXPIRE_TIME_MS)); michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: this._setResourceTTL(EXPIRE_TIME_MS); michael@0: } michael@0: catch (ex) { michael@0: this.abort(ex); michael@0: } michael@0: finally { michael@0: if (this._livemark.status == Ci.mozILivemark.STATUS_LOADING) { michael@0: this._livemark.status = Ci.mozILivemark.STATUS_READY; michael@0: } michael@0: this._livemark.locked = false; michael@0: this._livemark.loadGroup = null; michael@0: } michael@0: }, michael@0: michael@0: _setResourceTTL: function LLL__setResourceTTL(aMilliseconds) michael@0: { michael@0: this._livemark.expireTime = Date.now() + aMilliseconds; michael@0: }, michael@0: michael@0: // nsIInterfaceRequestor michael@0: getInterface: function LLL_getInterface(aIID) michael@0: { michael@0: return this.QueryInterface(aIID); michael@0: }, michael@0: michael@0: // nsISupports michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsIFeedResultListener michael@0: , Ci.nsIStreamListener michael@0: , Ci.nsIRequestObserver michael@0: , Ci.nsIInterfaceRequestor michael@0: ]) michael@0: } michael@0: michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LivemarkService]);