gecko/services/sync/modules/engines/bookmarks.js

826 lines
27 KiB
JavaScript
Raw Normal View History

const EXPORTED_SYMBOLS = ['BookmarksEngine'];
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://weave/log4moz.js");
Cu.import("resource://weave/dav.js");
Cu.import("resource://weave/util.js");
Cu.import("resource://weave/crypto.js");
Cu.import("resource://weave/async.js");
Cu.import("resource://weave/engines.js");
Cu.import("resource://weave/syncCores.js");
Cu.import("resource://weave/stores.js");
Cu.import("resource://weave/trackers.js");
Function.prototype.async = Async.sugar;
function BookmarksEngine(pbeId) {
this._init(pbeId);
}
BookmarksEngine.prototype = {
get name() { return "bookmarks"; },
get logName() { return "BmkEngine"; },
get serverPrefix() { return "user-data/bookmarks/"; },
__core: null,
get _core() {
if (!this.__core)
this.__core = new BookmarksSyncCore();
return this.__core;
},
__store: null,
get _store() {
if (!this.__store)
this.__store = new BookmarksStore();
return this.__store;
},
__tracker: null,
get _tracker() {
if (!this.__tracker)
this.__tracker = new BookmarksTracker();
return this.__tracker;
},
syncMounts: function BmkEngine_syncMounts(onComplete) {
this._syncMounts.async(this, onComplete);
},
_syncMounts: function BmkEngine__syncMounts() {
let self = yield;
let mounts = this._store.findMounts();
for (i = 0; i < mounts.length; i++) {
try {
this._syncOneMount.async(this, self.cb, mounts[i]);
yield;
} catch (e) {
this._log.warn("Could not sync shared folder from " + mounts[i].userid);
this._log.trace(Utils.stackTrace(e));
}
}
},
// TODO modify this as neccessary since I just moved it from the engine
// superclass into BookmarkEngine.
_share: function BookmarkEngine__share(guid, username) {
let self = yield;
let prefix = DAV.defaultPrefix;
this._log.debug("Sharing bookmarks with " + username);
this._getSymKey.async(this, self.cb);
yield;
// copied from getSymKey
DAV.GET(this.keysFile, self.cb);
let ret = yield;
Utils.ensureStatus(ret.status, "Could not get keys file.");
let keys = this._json.decode(ret.responseText);
// get the other user's pubkey
let serverURL = Utils.prefs.getCharPref("serverURL");
try {
DAV.defaultPrefix = "user/" + username + "/";
DAV.GET("public/pubkey", self.cb);
ret = yield;
}
catch (e) { throw e; }
finally { DAV.defaultPrefix = prefix; }
Utils.ensureStatus(ret.status, "Could not get public key for " + username);
let id = new Identity();
id.pubkey = ret.responseText;
// now encrypt the symkey with their pubkey and upload the new keyring
Crypto.RSAencrypt.async(Crypto, self.cb, this._engineId.password, id);
let enckey = yield;
if (!enckey)
throw "Could not encrypt symmetric encryption key";
keys.ring[username] = enckey;
DAV.PUT(this.keysFile, this._json.encode(keys), self.cb);
ret = yield;
Utils.ensureStatus(ret.status, "Could not upload keyring file.");
this._createShare(guid, username, username);
this._log.debug("All done sharing!");
self.done(true);
},
_createShare: function BookmarkEngine__createShare(guid, id, title) {
/* the bookmark item identified by guid, and the whole subtree under it,
must be copied out from the main file into a separate file which is
put into the new directory and encrypted with the key in the keychain.
id is the userid of the user we're sharing with.
*/
/* TODO it appears that this just creates the folder and puts the
annotation on it; the mechanics of sharing must be done when syncing?
Or has that not been done yet at all?
Do we have to create a new Mount?
*/
let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
getService(Ci.nsINavBookmarksService);
let ans = Cc["@mozilla.org/browser/annotation-service;1"].
getService(Ci.nsIAnnotationService);
let root;
let a = ans.getItemsWithAnnotation("weave/mounted-shares-folder", {});
if (a.length == 1)
root = a[0];
if (!root) {
root = bms.createFolder(bms.toolbarFolder, "Shared Folders",
bms.DEFAULT_INDEX);
ans.setItemAnnotation(root, "weave/mounted-shares-folder", true, 0,
ans.EXPIRE_NEVER);
}
let item;
a = ans.getItemsWithAnnotation("weave/mounted-share-id", {});
for (let i = 0; i < a.length; i++) {
if (ans.getItemAnnotation(a[i], "weave/mounted-share-id") == id) {
item = a[i];
break;
}
}
if (!item) {
let newId = bms.createFolder(root, title, bms.DEFAULT_INDEX);
ans.setItemAnnotation(newId, "weave/mounted-share-id", id, 0,
ans.EXPIRE_NEVER);
}
},
_stopShare: function BookmarkeEngine__stopShare( guid, username) {
// TODO implement this; also give a way to call it from outside
// the service.
},
_syncOneMount: function BmkEngine__syncOneMount(mountData) {
let self = yield;
let user = mountData.userid;
let prefix = DAV.defaultPrefix;
let serverURL = Utils.prefs.getCharPref("serverURL");
let snap = new SnapshotStore();
// TODO this is obviously what we want.
this._log.debug("Syncing shared folder from user " + user);
try {
DAV.defaultPrefix = "user/" + user + "/";
this._getSymKey.async(this, self.cb);
yield;
this._log.trace("Getting status file for " + user);
DAV.GET(this.statusFile, self.cb);
let resp = yield;
Utils.ensureStatus(resp.status, "Could not download status file.");
let status = this._json.decode(resp.responseText);
this._log.trace("Downloading server snapshot for " + user);
DAV.GET(this.snapshotFile, self.cb);
resp = yield;
Utils.ensureStatus(resp.status, "Could not download snapshot.");
Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText,
this._engineId, status.snapEncryption);
let data = yield;
snap.data = this._json.decode(data);
this._log.trace("Downloading server deltas for " + user);
DAV.GET(this.deltasFile, self.cb);
resp = yield;
Utils.ensureStatus(resp.status, "Could not download deltas.");
Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText,
this._engineId, status.deltasEncryption);
data = yield;
deltas = this._json.decode(data);
}
catch (e) { throw e; }
finally { DAV.defaultPrefix = prefix; }
// apply deltas to get current snapshot
for (var i = 0; i < deltas.length; i++) {
snap.applyCommands.async(snap, self.cb, deltas[i]);
yield;
}
// prune tree / get what we want
for (let guid in snap.data) {
if (snap.data[guid].type != "bookmark")
delete snap.data[guid];
else
snap.data[guid].parentGUID = mountData.rootGUID;
}
this._log.trace("Got bookmarks fror " + user + ", comparing with local copy");
this._core.detectUpdates(self.cb, mountData.snapshot, snap.data);
let diff = yield;
// FIXME: should make sure all GUIDs here live under the mountpoint
this._log.trace("Applying changes to folder from " + user);
this._store.applyCommands.async(this._store, self.cb, diff);
yield;
this._log.trace("Shared folder from " + user + " successfully synced!");
}
};
BookmarksEngine.prototype.__proto__ = new Engine();
function BookmarksSyncCore() {
this._init();
}
BookmarksSyncCore.prototype = {
_logName: "BMSync",
__bms: null,
get _bms() {
if (!this.__bms)
this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
getService(Ci.nsINavBookmarksService);
return this.__bms;
},
_itemExists: function BSC__itemExists(GUID) {
return this._bms.getItemIdForGUID(GUID) >= 0;
},
_getEdits: function BSC__getEdits(a, b) {
// NOTE: we do not increment ret.numProps, as that would cause
// edit commands to always get generated
let ret = SyncCore.prototype._getEdits.call(this, a, b);
ret.props.type = a.type;
return ret;
},
// compares properties
// returns true if the property is not set in either object
// returns true if the property is set and equal in both objects
// returns false otherwise
_comp: function BSC__comp(a, b, prop) {
return (!a.data[prop] && !b.data[prop]) ||
(a.data[prop] && b.data[prop] && (a.data[prop] == b.data[prop]));
},
_commandLike: function BSC__commandLike(a, b) {
// Check that neither command is null, that their actions, types,
// and parents are the same, and that they don't have the same
// GUID.
// * Items with the same GUID do not qualify for 'likeness' because
// we already consider them to be the same object, and therefore
// we need to process any edits.
// * Remove or edit commands don't qualify for likeness either,
// since remove or edit commands with different GUIDs are
// guaranteed to refer to two different items
// * The parent GUID check works because reconcile() fixes up the
// parent GUIDs as it runs, and the command list is sorted by
// depth
if (!a || !b ||
a.action != b.action ||
a.action != "create" ||
a.data.type != b.data.type ||
a.data.parentGUID != b.data.parentGUID ||
a.GUID == b.GUID)
return false;
// Bookmarks and folders are allowed to be in a different index as long as
// they are in the same folder. Separators must be at
// the same index to qualify for 'likeness'.
switch (a.data.type) {
case "bookmark":
if (this._comp(a, b, 'URI') &&
this._comp(a, b, 'title'))
return true;
return false;
case "query":
if (this._comp(a, b, 'URI') &&
this._comp(a, b, 'title'))
return true;
return false;
case "microsummary":
if (this._comp(a, b, 'URI') &&
this._comp(a, b, 'generatorURI'))
return true;
return false;
case "folder":
if (this._comp(a, b, 'title'))
return true;
return false;
case "livemark":
if (this._comp(a, b, 'title') &&
this._comp(a, b, 'siteURI') &&
this._comp(a, b, 'feedURI'))
return true;
return false;
case "separator":
if (this._comp(a, b, 'index'))
return true;
return false;
default:
let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
this._log.error("commandLike: Unknown item type: " + json.encode(a));
return false;
}
}
};
BookmarksSyncCore.prototype.__proto__ = new SyncCore();
function BookmarksStore() {
this._init();
}
BookmarksStore.prototype = {
_logName: "BStore",
__bms: null,
get _bms() {
if (!this.__bms)
this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
getService(Ci.nsINavBookmarksService);
return this.__bms;
},
__hsvc: null,
get _hsvc() {
if (!this.__hsvc)
this.__hsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
getService(Ci.nsINavHistoryService);
return this.__hsvc;
},
__ls: null,
get _ls() {
if (!this.__ls)
this.__ls = Cc["@mozilla.org/browser/livemark-service;2"].
getService(Ci.nsILivemarkService);
return this.__ls;
},
__ms: null,
get _ms() {
if (!this.__ms)
this.__ms = Cc["@mozilla.org/microsummary/service;1"].
getService(Ci.nsIMicrosummaryService);
return this.__ms;
},
__ts: null,
get _ts() {
if (!this.__ts)
this.__ts = Cc["@mozilla.org/browser/tagging-service;1"].
getService(Ci.nsITaggingService);
return this.__ts;
},
__ans: null,
get _ans() {
if (!this.__ans)
this.__ans = Cc["@mozilla.org/browser/annotation-service;1"].
getService(Ci.nsIAnnotationService);
return this.__ans;
},
_getItemIdForGUID: function BStore__getItemIdForGUID(GUID) {
switch (GUID) {
case "menu":
return this._bms.bookmarksMenuFolder;
case "toolbar":
return this._bms.toolbarFolder;
case "unfiled":
return this._bms.unfiledBookmarksFolder;
default:
return this._bms.getItemIdForGUID(GUID);
}
return null;
},
_createCommand: function BStore__createCommand(command) {
let newId;
let parentId = this._getItemIdForGUID(command.data.parentGUID);
if (parentId < 0) {
this._log.warn("Creating node with unknown parent -> reparenting to root");
parentId = this._bms.bookmarksMenuFolder;
}
switch (command.data.type) {
case "query":
case "bookmark":
case "microsummary": {
this._log.debug(" -> creating bookmark \"" + command.data.title + "\"");
let URI = Utils.makeURI(command.data.URI);
newId = this._bms.insertBookmark(parentId,
URI,
command.data.index,
command.data.title);
this._ts.untagURI(URI, null);
this._ts.tagURI(URI, command.data.tags);
this._bms.setKeywordForBookmark(newId, command.data.keyword);
if (command.data.description) {
this._ans.setItemAnnotation(newId, "bookmarkProperties/description",
command.data.description, 0,
this._ans.EXPIRE_NEVER);
}
if (command.data.type == "microsummary") {
this._log.debug(" \-> is a microsummary");
this._ans.setItemAnnotation(newId, "bookmarks/staticTitle",
command.data.staticTitle || "", 0, this._ans.EXPIRE_NEVER);
let genURI = Utils.makeURI(command.data.generatorURI);
try {
let micsum = this._ms.createMicrosummary(URI, genURI);
this._ms.setMicrosummary(newId, micsum);
}
catch(ex) { /* ignore "missing local generator" exceptions */ }
}
} break;
case "folder":
this._log.debug(" -> creating folder \"" + command.data.title + "\"");
newId = this._bms.createFolder(parentId,
command.data.title,
command.data.index);
break;
case "livemark":
this._log.debug(" -> creating livemark \"" + command.data.title + "\"");
newId = this._ls.createLivemark(parentId,
command.data.title,
Utils.makeURI(command.data.siteURI),
Utils.makeURI(command.data.feedURI),
command.data.index);
break;
case "mounted-share":
// TODO this is to create the shared-items folder on another machine
// to duplicate the one that's on the first machine; we don't need that
// anymore. OR is it to create the folder on the sharee's computer?
this._log.debug(" -> creating share mountpoint \"" + command.data.title + "\"");
newId = this._bms.createFolder(parentId,
command.data.title,
command.data.index);
this._ans.setItemAnnotation(newId, "weave/mounted-share-id",
command.data.mountId, 0, this._ans.EXPIRE_NEVER);
break;
case "separator":
this._log.debug(" -> creating separator");
newId = this._bms.insertSeparator(parentId, command.data.index);
break;
default:
this._log.error("_createCommand: Unknown item type: " + command.data.type);
break;
}
if (newId)
this._bms.setItemGUID(newId, command.GUID);
},
_removeCommand: function BStore__removeCommand(command) {
if (command.GUID == "menu" ||
command.GUID == "toolbar" ||
command.GUID == "unfiled") {
this._log.warn("Attempted to remove root node (" + command.GUID +
"). Skipping command.");
return;
}
var itemId = this._bms.getItemIdForGUID(command.GUID);
if (itemId < 0) {
this._log.warn("Attempted to remove item " + command.GUID +
", but it does not exist. Skipping.");
return;
}
var type = this._bms.getItemType(itemId);
switch (type) {
case this._bms.TYPE_BOOKMARK:
this._log.debug(" -> removing bookmark " + command.GUID);
this._bms.removeItem(itemId);
break;
case this._bms.TYPE_FOLDER:
this._log.debug(" -> removing folder " + command.GUID);
this._bms.removeFolder(itemId);
break;
case this._bms.TYPE_SEPARATOR:
this._log.debug(" -> removing separator " + command.GUID);
this._bms.removeItem(itemId);
break;
default:
this._log.error("removeCommand: Unknown item type: " + type);
break;
}
},
_editCommand: function BStore__editCommand(command) {
if (command.GUID == "menu" ||
command.GUID == "toolbar" ||
command.GUID == "unfiled") {
this._log.warn("Attempted to edit root node (" + command.GUID +
"). Skipping command.");
return;
}
var itemId = this._bms.getItemIdForGUID(command.GUID);
if (itemId < 0) {
this._log.warn("Item for GUID " + command.GUID + " not found. Skipping.");
return;
}
for (let key in command.data) {
switch (key) {
case "type":
// all commands have this to help in reconciliation, but it makes
// no sense to edit it
break;
case "GUID":
var existing = this._getItemIdForGUID(command.data.GUID);
if (existing < 0)
this._bms.setItemGUID(itemId, command.data.GUID);
else
this._log.warn("Can't change GUID " + command.GUID +
" to " + command.data.GUID + ": GUID already exists.");
break;
case "title":
this._bms.setItemTitle(itemId, command.data.title);
break;
case "URI":
this._bms.changeBookmarkURI(itemId, Utils.makeURI(command.data.URI));
break;
case "index":
this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId),
command.data.index);
break;
case "parentGUID": {
let index = -1;
if (command.data.index && command.data.index >= 0)
index = command.data.index;
this._bms.moveItem(
itemId, this._getItemIdForGUID(command.data.parentGUID), index);
} break;
case "tags": {
let tagsURI = this._bms.getBookmarkURI(itemId);
this._ts.untagURI(tagsURI, null);
this._ts.tagURI(tagsURI, command.data.tags);
} break;
case "keyword":
this._bms.setKeywordForBookmark(itemId, command.data.keyword);
break;
case "description":
if (command.data.description) {
this._ans.setItemAnnotation(itemId, "bookmarkProperties/description",
command.data.description, 0,
this._ans.EXPIRE_NEVER);
}
break;
case "generatorURI": {
let micsumURI = Utils.makeURI(this._bms.getBookmarkURI(itemId));
let genURI = Utils.makeURI(command.data.generatorURI);
let micsum = this._ms.createMicrosummary(micsumURI, genURI);
this._ms.setMicrosummary(itemId, micsum);
} break;
case "siteURI":
this._ls.setSiteURI(itemId, Utils.makeURI(command.data.siteURI));
break;
case "feedURI":
this._ls.setFeedURI(itemId, Utils.makeURI(command.data.feedURI));
break;
default:
this._log.warn("Can't change item property: " + key);
break;
}
}
},
_getNode: function BSS__getNode(folder) {
let query = this._hsvc.getNewQuery();
query.setFolders([folder], 1);
return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root;
},
__wrap: function BSS___wrap(node, items, parentGUID, index, guidOverride) {
let GUID, item;
// we override the guid for the root items, "menu", "toolbar", etc.
if (guidOverride) {
GUID = guidOverride;
item = {};
} else {
GUID = this._bms.getItemGUID(node.itemId);
item = {parentGUID: parentGUID, index: index};
}
if (node.type == node.RESULT_TYPE_FOLDER) {
if (this._ls.isLivemark(node.itemId)) {
item.type = "livemark";
let siteURI = this._ls.getSiteURI(node.itemId);
let feedURI = this._ls.getFeedURI(node.itemId);
item.siteURI = siteURI? siteURI.spec : "";
item.feedURI = feedURI? feedURI.spec : "";
} else if (this._ans.itemHasAnnotation(node.itemId,
"weave/mounted-share-id")) {
/* TODO this is for wrapping the special shared folder created by
the old-style share command. */
item.type = "mounted-share";
item.title = node.title;
item.mountId = this._ans.getItemAnnotation(node.itemId,
"weave/mounted-share-id");
} else {
item.type = "folder";
node.QueryInterface(Ci.nsINavHistoryQueryResultNode);
node.containerOpen = true;
for (var i = 0; i < node.childCount; i++) {
this.__wrap(node.getChild(i), items, GUID, i);
}
}
if (!guidOverride)
item.title = node.title; // no titles for root nodes
} else if (node.type == node.RESULT_TYPE_URI ||
node.type == node.RESULT_TYPE_QUERY) {
if (this._ms.hasMicrosummary(node.itemId)) {
item.type = "microsummary";
let micsum = this._ms.getMicrosummary(node.itemId);
item.generatorURI = micsum.generator.uri.spec; // breaks local generators
item.staticTitle = this._ans.getItemAnnotation(node.itemId, "bookmarks/staticTitle");
} else if (node.type == node.RESULT_TYPE_QUERY) {
item.type = "query";
item.title = node.title;
} else {
item.type = "bookmark";
item.title = node.title;
}
try {
item.description =
this._ans.getItemAnnotation(node.itemId, "bookmarkProperties/description");
} catch (e) {
item.description = undefined;
}
item.URI = node.uri;
// This will throw if makeURI can't make an nsIURI object out of the
// node.uri string (or return null if node.uri is null), in which case
// we won't be able to get tags for the bookmark (but we'll still sync
// the rest of the record).
let uri;
try {
uri = Utils.makeURI(node.uri);
}
catch(e) {
this._log.error("error parsing URI string <" + node.uri + "> " +
"for item " + node.itemId + " (" + node.title + "): " +
e);
}
if (uri)
item.tags = this._ts.getTagsForURI(uri, {});
item.keyword = this._bms.getKeywordForBookmark(node.itemId);
} else if (node.type == node.RESULT_TYPE_SEPARATOR) {
item.type = "separator";
} else {
this._log.warn("Warning: unknown item type, cannot serialize: " + node.type);
return;
}
items[GUID] = item;
},
// helper
_wrap: function BStore__wrap(node, items, rootName) {
return this.__wrap(node, items, null, null, rootName);
},
_wrapMount: function BStore__wrapMount(node, id) {
if (node.type != node.RESULT_TYPE_FOLDER)
throw "Trying to wrap a non-folder mounted share";
let GUID = this._bms.getItemGUID(node.itemId);
let ret = {rootGUID: GUID, userid: id, snapshot: {}};
node.QueryInterface(Ci.nsINavHistoryQueryResultNode);
node.containerOpen = true;
for (var i = 0; i < node.childCount; i++) {
this.__wrap(node.getChild(i), ret.snapshot, GUID, i);
}
// remove any share mountpoints
for (let guid in ret.snapshot) {
// TODO decide what to do with this...
if (ret.snapshot[guid].type == "mounted-share")
delete ret.snapshot[guid];
}
return ret;
},
_resetGUIDs: function BSS__resetGUIDs(node) {
if (this._ans.itemHasAnnotation(node.itemId, "placesInternal/GUID"))
this._ans.removeItemAnnotation(node.itemId, "placesInternal/GUID");
if (node.type == node.RESULT_TYPE_FOLDER &&
!this._ls.isLivemark(node.itemId)) {
node.QueryInterface(Ci.nsINavHistoryQueryResultNode);
node.containerOpen = true;
for (var i = 0; i < node.childCount; i++) {
this._resetGUIDs(node.getChild(i));
}
}
},
findMounts: function BStore_findMounts() {
let ret = [];
let a = this._ans.getItemsWithAnnotation("weave/mounted-share-id", {});
for (let i = 0; i < a.length; i++) {
let id = this._ans.getItemAnnotation(a[i], "weave/mounted-share-id");
ret.push(this._wrapMount(this._getNode(a[i]), id));
}
return ret;
},
wrap: function BStore_wrap() {
var items = {};
this._wrap(this._getNode(this._bms.bookmarksMenuFolder), items, "menu");
this._wrap(this._getNode(this._bms.toolbarFolder), items, "toolbar");
this._wrap(this._getNode(this._bms.unfiledBookmarksFolder), items, "unfiled");
return items;
},
wipe: function BStore_wipe() {
this._bms.removeFolderChildren(this._bms.bookmarksMenuFolder);
this._bms.removeFolderChildren(this._bms.toolbarFolder);
this._bms.removeFolderChildren(this._bms.unfiledBookmarksFolder);
},
resetGUIDs: function BStore_resetGUIDs() {
this._resetGUIDs(this._getNode(this._bms.bookmarksMenuFolder));
this._resetGUIDs(this._getNode(this._bms.toolbarFolder));
this._resetGUIDs(this._getNode(this._bms.unfiledBookmarksFolder));
}
};
BookmarksStore.prototype.__proto__ = new Store();
/*
* Tracker objects for each engine may need to subclass the
* getScore routine, which returns the current 'score' for that
* engine. How the engine decides to set the score is upto it,
* as long as the value between 0 and 100 actually corresponds
* to its urgency to sync.
*
* Here's an example BookmarksTracker. We don't subclass getScore
* because the observer methods take care of updating _score which
* getScore returns by default.
*/
function BookmarksTracker() {
this._init();
}
BookmarksTracker.prototype = {
_logName: "BMTracker",
/* We don't care about the first three */
onBeginUpdateBatch: function BMT_onBeginUpdateBatch() {
},
onEndUpdateBatch: function BMT_onEndUpdateBatch() {
},
onItemVisited: function BMT_onItemVisited() {
},
/* Every add or remove is worth 4 points,
* on the basis that adding or removing 20 bookmarks
* means its time to sync?
*/
onItemAdded: function BMT_onEndUpdateBatch() {
this._score += 4;
},
onItemRemoved: function BMT_onItemRemoved() {
this._score += 4;
},
/* Changes are worth 2 points? */
onItemChanged: function BMT_onItemChanged() {
this._score += 2;
},
_init: function BMT__init() {
this._log = Log4Moz.Service.getLogger("Service." + this._logName);
this._score = 0;
Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
getService(Ci.nsINavBookmarksService).
addObserver(this, false);
}
}
BookmarksTracker.prototype.__proto__ = new Tracker();