gecko-dev/toolkit/components/places/nsLivemarkService.js

1056 lines
33 KiB
JavaScript

/* 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 Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
////////////////////////////////////////////////////////////////////////////////
//// Modules
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, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
"resource://gre/modules/Deprecated.jsm");
////////////////////////////////////////////////////////////////////////////////
//// Services
XPCOMUtils.defineLazyServiceGetter(this, "secMan",
"@mozilla.org/scriptsecuritymanager;1",
"nsIScriptSecurityManager");
XPCOMUtils.defineLazyGetter(this, "asyncHistory", function () {
// Lazily add an history observer when it's actually needed.
PlacesUtils.history.addObserver(PlacesUtils.livemarks, true);
return Cc["@mozilla.org/browser/history;1"].getService(Ci.mozIAsyncHistory);
});
////////////////////////////////////////////////////////////////////////////////
//// Constants
// Security flags for checkLoadURIWithPrincipal.
const SEC_FLAGS = Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL;
// 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.
////////////////////////////////////////////////////////////////////////////////
//// LivemarkService
function LivemarkService()
{
// Cleanup on shutdown.
Services.obs.addObserver(this, PlacesUtils.TOPIC_SHUTDOWN, true);
// Observe bookmarks and history, but don't init the services just for that.
PlacesUtils.addLazyBookmarkObserver(this, true);
// Asynchronously build the livemarks cache.
this._ensureAsynchronousCache();
}
LivemarkService.prototype = {
// Cache of Livemark objects, hashed by bookmarks folder ids.
_livemarks: {},
// Hash associating guids to bookmarks folder ids.
_guids: {},
get _populateCacheSQL()
{
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, b.position, b.guid, b.lastModified, "
+ "(" + getAnnoSQLFragment(":feedURI_anno") + ") AS feedURI, "
+ "(" + getAnnoSQLFragment(":siteURI_anno") + ") AS siteURI "
+ "FROM moz_bookmarks b "
+ "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 ";
},
_ensureAsynchronousCache: function LS__ensureAsynchronousCache()
{
let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
.DBConnection;
let stmt = db.createAsyncStatement(this._populateCacheSQL);
stmt.params.folder_type = Ci.nsINavBookmarksService.TYPE_FOLDER;
stmt.params.feedURI_anno = PlacesUtils.LMANNO_FEEDURI;
stmt.params.siteURI_anno = PlacesUtils.LMANNO_SITEURI;
let livemarkSvc = this;
this._pendingStmt = stmt.executeAsync({
handleResult: function LS_handleResult(aResults)
{
for (let row = aResults.getNextRow(); row; row = aResults.getNextRow()) {
let id = row.getResultByName("id");
let siteURL = row.getResultByName("siteURI");
let guid = row.getResultByName("guid");
livemarkSvc._livemarks[id] =
new Livemark({ id: id,
guid: guid,
title: row.getResultByName("title"),
parentId: row.getResultByName("parent"),
index: row.getResultByName("position"),
lastModified: row.getResultByName("lastModified"),
feedURI: NetUtil.newURI(row.getResultByName("feedURI")),
siteURI: siteURL ? NetUtil.newURI(siteURL) : null,
});
livemarkSvc._guids[guid] = id;
}
},
handleError: function LS_handleError(aErr)
{
Cu.reportError("AsyncStmt error (" + aErr.result + "): '" + aErr.message);
},
handleCompletion: function LS_handleCompletion() {
livemarkSvc._pendingStmt = null;
}
});
stmt.finalize();
},
_onCacheReady: function LS__onCacheReady(aCallback)
{
if (this._pendingStmt) {
// The cache is still being populated, so enqueue the job to the Storage
// async thread. Ideally this should just dispatch a runnable to it,
// that would call back on the main thread, but bug 608142 made that
// impossible. Thus just enqueue the cheapest query possible.
let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
.DBConnection;
let stmt = db.createAsyncStatement("PRAGMA encoding");
stmt.executeAsync({
handleError: function () {},
handleResult: function () {},
handleCompletion: function ETAT_handleCompletion()
{
aCallback();
}
});
stmt.finalize();
}
else {
// The callbacks should always be enqueued per the interface.
// Just enque on the main thread.
Services.tm.mainThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL);
}
},
_reloading: false,
_startReloadTimer: function LS__startReloadTimer()
{
if (this._reloadTimer) {
this._reloadTimer.cancel();
}
else {
this._reloadTimer = Cc["@mozilla.org/timer;1"]
.createInstance(Ci.nsITimer);
}
this._reloading = true;
this._reloadTimer.initWithCallback(this._reloadNextLivemark.bind(this),
RELOAD_DELAY_MS,
Ci.nsITimer.TYPE_ONE_SHOT);
},
//////////////////////////////////////////////////////////////////////////////
//// nsIObserver
observe: function LS_observe(aSubject, aTopic, aData)
{
if (aTopic == PlacesUtils.TOPIC_SHUTDOWN) {
if (this._pendingStmt) {
this._pendingStmt.cancel();
this._pendingStmt = null;
// Initialization never finished, so just bail out.
return;
}
if (this._reloadTimer) {
this._reloading = false;
this._reloadTimer.cancel();
delete this._reloadTimer;
}
// Stop any ongoing update.
for each (let livemark in this._livemarks) {
livemark.terminate();
}
this._livemarks = {};
}
},
//////////////////////////////////////////////////////////////////////////////
//// mozIAsyncLivemarks
addLivemark: function LS_addLivemark(aLivemarkInfo,
aLivemarkCallback)
{
// Must provide at least non-null parentId, index and feedURI.
if (!aLivemarkInfo ||
("parentId" in aLivemarkInfo && aLivemarkInfo.parentId < 1) ||
!("index" in aLivemarkInfo) || 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 Cr.NS_ERROR_INVALID_ARG;
}
if (aLivemarkCallback) {
Deprecated.warning("Passing a callback to Livermarks methods is deprecated. " +
"Please use the returned promise instead.",
"https://developer.mozilla.org/docs/Mozilla/JavaScript_code_modules/Promise.jsm");
}
// The addition is done synchronously due to the fact importExport service
// and JSON backups require that. The notification is async though.
// Once bookmarks are async, this may be properly fixed.
let deferred = Promise.defer();
let addLivemarkEx = null;
let livemark = null;
try {
// Disallow adding a livemark inside another livemark.
if (aLivemarkInfo.parentId in this._livemarks) {
throw new Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
}
// Don't pass unexpected input data to the livemark constructor.
livemark = new Livemark({ title: aLivemarkInfo.title
, parentId: aLivemarkInfo.parentId
, index: aLivemarkInfo.index
, feedURI: aLivemarkInfo.feedURI
, siteURI: aLivemarkInfo.siteURI
, guid: aLivemarkInfo.guid
, lastModified: aLivemarkInfo.lastModified
});
if (this._itemAdded && this._itemAdded.id == livemark.id) {
livemark.index = this._itemAdded.index;
livemark.guid = this._itemAdded.guid;
if (!aLivemarkInfo.lastModified) {
livemark.lastModified = this._itemAdded.lastModified;
}
}
// Updating the cache even if it has not yet been populated doesn't
// matter since it will just be overwritten.
this._livemarks[livemark.id] = livemark;
this._guids[livemark.guid] = livemark.id;
}
catch (ex) {
addLivemarkEx = ex;
livemark = null;
}
finally {
this._onCacheReady( () => {
if (addLivemarkEx) {
if (aLivemarkCallback) {
try {
aLivemarkCallback.onCompletion(addLivemarkEx.result, livemark);
}
catch(ex2) { }
} else {
deferred.reject(addLivemarkEx);
}
}
else {
if (aLivemarkCallback) {
try {
aLivemarkCallback.onCompletion(Cr.NS_OK, livemark);
}
catch(ex2) { }
} else {
deferred.resolve(livemark);
}
}
});
}
return aLivemarkCallback ? null : deferred.promise;
},
removeLivemark: function LS_removeLivemark(aLivemarkInfo, aLivemarkCallback)
{
if (!aLivemarkInfo) {
throw Cr.NS_ERROR_INVALID_ARG;
}
// Accept either a guid or an id.
let id = aLivemarkInfo.guid || aLivemarkInfo.id;
if (("guid" in aLivemarkInfo && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) ||
("id" in aLivemarkInfo && aLivemarkInfo.id < 1) ||
!id) {
throw Cr.NS_ERROR_INVALID_ARG;
}
if (aLivemarkCallback) {
Deprecated.warning("Passing a callback to Livermarks methods is deprecated. " +
"Please use the returned promise instead.",
"https://developer.mozilla.org/docs/Mozilla/JavaScript_code_modules/Promise.jsm");
}
// Convert the guid to an id.
if (id in this._guids) {
id = this._guids[id];
}
let deferred = Promise.defer();
let removeLivemarkEx = null;
try {
if (!(id in this._livemarks)) {
throw new Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
}
this._livemarks[id].remove();
}
catch (ex) {
removeLivemarkEx = ex;
}
finally {
this._onCacheReady( () => {
if (removeLivemarkEx) {
if (aLivemarkCallback) {
try {
aLivemarkCallback.onCompletion(removeLivemarkEx.result, null);
}
catch(ex2) { }
} else {
deferred.reject(removeLivemarkEx);
}
}
else {
if (aLivemarkCallback) {
try {
aLivemarkCallback.onCompletion(Cr.NS_OK, null);
}
catch(ex2) { }
} else {
deferred.resolve();
}
}
});
}
return aLivemarkCallback ? null : deferred.promise;
},
_reloaded: [],
_reloadNextLivemark: function LS__reloadNextLivemark()
{
this._reloading = false;
// Find first livemark to be reloaded.
for (let id in this._livemarks) {
if (this._reloaded.indexOf(id) == -1) {
this._reloaded.push(id);
this._livemarks[id].reload(this._forceUpdate);
this._startReloadTimer();
break;
}
}
},
reloadLivemarks: function LS_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._onCacheReady( () => {
this._forceUpdate = !!aForceUpdate;
this._reloaded = [];
// Livemarks reloads happen on a timer, and are delayed for performance
// reasons.
this._startReloadTimer();
});
},
getLivemark: function LS_getLivemark(aLivemarkInfo, aLivemarkCallback)
{
if (!aLivemarkInfo) {
throw Cr.NS_ERROR_INVALID_ARG;
}
// Accept either a guid or an id.
let id = aLivemarkInfo.guid || aLivemarkInfo.id;
if (("guid" in aLivemarkInfo && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) ||
("id" in aLivemarkInfo && aLivemarkInfo.id < 1) ||
!id) {
throw Cr.NS_ERROR_INVALID_ARG;
}
if (aLivemarkCallback) {
Deprecated.warning("Passing a callback to Livermarks methods is deprecated. " +
"Please use the returned promise instead.",
"https://developer.mozilla.org/docs/Mozilla/JavaScript_code_modules/Promise.jsm");
}
let deferred = Promise.defer();
this._onCacheReady( () => {
// Convert the guid to an id.
if (id in this._guids) {
id = this._guids[id];
}
if (id in this._livemarks) {
if (aLivemarkCallback) {
try {
aLivemarkCallback.onCompletion(Cr.NS_OK, this._livemarks[id]);
} catch (ex) {}
} else {
deferred.resolve(this._livemarks[id]);
}
}
else {
if (aLivemarkCallback) {
try {
aLivemarkCallback.onCompletion(Cr.NS_ERROR_INVALID_ARG, null);
} catch (ex) { }
} else {
deferred.reject(Components.Exception("", Cr.NS_ERROR_INVALID_ARG));
}
}
});
return aLivemarkCallback ? null : deferred.promise;
},
//////////////////////////////////////////////////////////////////////////////
//// nsINavBookmarkObserver
onBeginUpdateBatch: function () {},
onEndUpdateBatch: function () {},
onItemVisited: function () {},
_itemAdded: null,
onItemAdded: function LS_onItemAdded(aItemId, aParentId, aIndex, aItemType,
aURI, aTitle, aDateAdded, aGUID)
{
if (aItemType == Ci.nsINavBookmarksService.TYPE_FOLDER) {
this._itemAdded = { id: aItemId
, guid: aGUID
, index: aIndex
, lastModified: aDateAdded
};
}
},
onItemChanged: function LS_onItemChanged(aItemId, aProperty, aIsAnno, aValue,
aLastModified, aItemType)
{
if (aItemType == Ci.nsINavBookmarksService.TYPE_FOLDER) {
if (this._itemAdded && this._itemAdded.id == aItemId) {
this._itemAdded.lastModified = aLastModified;
}
if (aItemId in this._livemarks) {
if (aProperty == "title") {
this._livemarks[aItemId].title = aValue;
}
this._livemarks[aItemId].lastModified = aLastModified;
}
}
},
onItemMoved: function LS_onItemMoved(aItemId, aOldParentId, aOldIndex,
aNewParentId, aNewIndex, aItemType)
{
if (aItemType == Ci.nsINavBookmarksService.TYPE_FOLDER &&
aItemId in this._livemarks) {
this._livemarks[aItemId].parentId = aNewParentId;
this._livemarks[aItemId].index = aNewIndex;
}
},
onItemRemoved: function LS_onItemRemoved(aItemId, aParentId, aIndex,
aItemType, aURI, aGUID)
{
if (aItemType == Ci.nsINavBookmarksService.TYPE_FOLDER &&
aItemId in this._livemarks) {
this._livemarks[aItemId].terminate();
delete this._livemarks[aItemId];
delete this._guids[aGUID];
}
},
//////////////////////////////////////////////////////////////////////////////
//// nsINavHistoryObserver
onBeginUpdateBatch: function () {},
onEndUpdateBatch: function () {},
onPageChanged: function () {},
onTitleChanged: function () {},
onDeleteVisits: function () {},
onClearHistory: function () {},
onDeleteURI: function PS_onDeleteURI(aURI) {
for each (let livemark in this._livemarks) {
livemark.updateURIVisitedStatus(aURI, false);
}
},
onVisit: function PS_onVisit(aURI) {
for each (let livemark in this._livemarks) {
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.title = aLivemarkInfo.title;
this.parentId = aLivemarkInfo.parentId;
this.index = aLivemarkInfo.index;
this._status = Ci.mozILivemark.STATUS_READY;
// Hash of resultObservers, hashed by container.
this._resultObservers = new Map();
// This keeps a list of the containers used as keys in the map, since
// it's not iterable. In future may use an iterable Map.
this._resultObserversList = [];
// 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._guid = "";
this._lastModified = 0;
this.loadGroup = null;
this.feedURI = null;
this.siteURI = null;
this.expireTime = 0;
if (aLivemarkInfo.id) {
// This request comes from the cache.
this.id = aLivemarkInfo.id;
this.guid = aLivemarkInfo.guid;
this.feedURI = aLivemarkInfo.feedURI;
this.siteURI = aLivemarkInfo.siteURI;
this.lastModified = aLivemarkInfo.lastModified;
}
else {
// Create a new livemark.
this.id = PlacesUtils.bookmarks.createFolder(aLivemarkInfo.parentId,
aLivemarkInfo.title,
aLivemarkInfo.index,
aLivemarkInfo.guid);
PlacesUtils.bookmarks.setFolderReadonly(this.id, true);
this.writeFeedURI(aLivemarkInfo.feedURI);
if (aLivemarkInfo.siteURI) {
this.writeSiteURI(aLivemarkInfo.siteURI);
}
// Last modified time must be the last change.
if (aLivemarkInfo.lastModified) {
this.lastModified = aLivemarkInfo.lastModified;
PlacesUtils.bookmarks.setItemLastModified(this.id, this.lastModified);
}
}
}
Livemark.prototype = {
get status() this._status,
set status(val) {
if (this._status != val) {
this._status = val;
this._invalidateRegisteredContainers();
}
return this._status;
},
/**
* Sets an annotation on the bookmarks folder id representing the livemark.
*
* @param aAnnoName
* Name of the annotation.
* @param aValue
* Value of the annotation.
* @return The annotation value.
* @throws If the folder is invalid.
*/
_setAnno: function LM__setAnno(aAnnoName, aValue)
{
PlacesUtils.annotations
.setItemAnnotation(this.id, aAnnoName, aValue, 0,
PlacesUtils.annotations.EXPIRE_NEVER);
},
writeFeedURI: function LM_writeFeedURI(aFeedURI)
{
this._setAnno(PlacesUtils.LMANNO_FEEDURI, aFeedURI.spec);
this.feedURI = aFeedURI;
},
writeSiteURI: function LM_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 feedPrincipal = secMan.getSimpleCodebasePrincipal(this.feedURI);
try {
secMan.checkLoadURIWithPrincipal(feedPrincipal, aSiteURI, SEC_FLAGS);
}
catch (ex) {
return;
}
this._setAnno(PlacesUtils.LMANNO_SITEURI, aSiteURI.spec)
this.siteURI = aSiteURI;
},
set guid(aGUID) {
this._guid = aGUID;
return aGUID;
},
get guid() this._guid,
set lastModified(aLastModified) {
this._lastModified = aLastModified;
return aLastModified;
},
get lastModified() this._lastModified,
/**
* 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: function LM_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);
let channel = NetUtil.newChannel(this.feedURI.spec).
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.asyncOpen(listener, null);
this.loadGroup = loadgroup;
}
catch (ex) {
this.status = Ci.mozILivemark.STATUS_FAILED;
}
},
reload: function LM_reload(aForceUpdate)
{
this.updateChildren(aForceUpdate);
},
remove: function LM_remove() {
PlacesUtils.bookmarks.removeItem(this.id);
},
get children() this._children,
set children(val) {
this._children = val;
// Discard the previous cached nodes, new ones should be generated.
for (let i = 0; i < this._resultObserversList.length; i++) {
let container = this._resultObserversList[i];
this._nodes.delete(container);
}
// Update visited status for each entry.
for (let i = 0; i < this._children.length; i++) {
let child = this._children[i];
asyncHistory.isURIVisited(child.uri,
(function(aURI, aIsVisited) {
this.updateURIVisitedStatus(aURI, aIsVisited);
}).bind(this));
}
return this._children;
},
_isURIVisited: function LM__isURIVisited(aURI) {
for (let i = 0; i < this.children.length; i++) {
if (this.children[i].uri.equals(aURI)) {
return this.children[i].visited;
}
}
},
getNodesForContainer: function LM_getNodesForContainer(aContainerNode)
{
if (this._nodes.has(aContainerNode)) {
return this._nodes.get(aContainerNode);
}
let livemark = this;
let nodes = [];
let now = Date.now() * 1000;
for (let i = 0; i < this._children.length; i++) {
let child = this._children[i];
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()
aContainerNode.QueryInterface(Ci.nsINavHistoryContainerResultNode),
get parentResult() this.parent.parentResult,
get uri() child.uri.spec,
get type() Ci.nsINavHistoryResultNode.RESULT_TYPE_URI,
get title() child.title,
get accessCount()
Number(livemark._isURIVisited(NetUtil.newURI(this.uri))),
get time() 0,
get icon() "",
get indentLevel() this.parent.indentLevel + 1,
get bookmarkIndex() -1,
get itemId() -1,
get dateAdded() now + i,
get lastModified() now + i,
get tags()
PlacesUtils.tagging.getTagsForURI(NetUtil.newURI(this.uri)).join(", "),
QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryResultNode])
};
nodes.push(node);
}
this._nodes.set(aContainerNode, nodes);
return nodes;
},
registerForUpdates: function LM_registerForUpdates(aContainerNode,
aResultObserver)
{
this._resultObservers.set(aContainerNode, aResultObserver);
this._resultObserversList.push(aContainerNode);
},
unregisterForUpdates: function LM_unregisterForUpdates(aContainerNode)
{
this._resultObservers.delete(aContainerNode);
let index = this._resultObserversList.indexOf(aContainerNode);
this._resultObserversList.splice(index, 1);
this._nodes.delete(aContainerNode);
},
_invalidateRegisteredContainers: function LM__invalidateRegisteredContainers()
{
for (let i = 0; i < this._resultObserversList.length; i++) {
let container = this._resultObserversList[i];
let observer = this._resultObservers.get(container);
observer.invalidateContainer(container);
}
},
updateURIVisitedStatus:
function LM_updateURIVisitedStatus(aURI, aVisitedStatus)
{
for (let i = 0; i < this.children.length; i++) {
if (this.children[i].uri.equals(aURI)) {
this.children[i].visited = aVisitedStatus;
}
}
for (let i = 0; i < this._resultObserversList.length; i++) {
let container = this._resultObserversList[i];
let observer = this._resultObservers.get(container);
if (this._nodes.has(container)) {
let nodes = this._nodes.get(container);
for (let j = 0; j < nodes.length; j++) {
let node = nodes[j];
if (node.uri == aURI.spec) {
Services.tm.mainThread.dispatch((function () {
observer.nodeHistoryDetailsChanged(node, 0, aVisitedStatus);
}).bind(this), Ci.nsIThread.DISPATCH_NORMAL);
}
}
}
}
},
/**
* Terminates the livemark entry, cancelling any ongoing load.
* Must be invoked before destroying the entry.
*/
terminate: function LM_terminate()
{
// Avoid handling any updateChildren request from now on.
this._terminated = true;
// Clear the list before aborting, since abort() would try to set the
// status and notify about it, but that's not really useful at this point.
this._resultObserversList = [];
this.abort();
},
/**
* Aborts the livemark loading if needed.
*/
abort: function LM_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: function LLL_abort(aException)
{
if (!this._isAborted) {
this._isAborted = true;
this._livemark.abort();
this._setResourceTTL(ONERROR_EXPIRE_TIME_MS);
}
},
// nsIFeedResultListener
handleResult: function LLL_handleResult(aResult)
{
if (this._isAborted) {
return;
}
try {
// We need this to make sure the item links are safe
let feedPrincipal =
secMan.getSimpleCodebasePrincipal(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 {
secMan.checkLoadURIWithPrincipal(feedPrincipal, uri, SEC_FLAGS);
}
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: function LLL_onDataAvailable(aRequest, aContext,
aInputStream, aSourceOffset,
aCount)
{
if (this._processor) {
this._processor.onDataAvailable(aRequest, aContext, aInputStream,
aSourceOffset, aCount);
}
},
onStartRequest: function LLL_onStartRequest(aRequest, aContext)
{
if (this._isAborted) {
throw 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: function LLL_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: function LLL__setResourceTTL(aMilliseconds)
{
this._livemark.expireTime = Date.now() + aMilliseconds;
},
// nsIInterfaceRequestor
getInterface: function LLL_getInterface(aIID)
{
return this.QueryInterface(aIID);
},
// nsISupports
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIFeedResultListener
, Ci.nsIStreamListener
, Ci.nsIRequestObserver
, Ci.nsIInterfaceRequestor
])
}
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LivemarkService]);