mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-01 14:45:29 +00:00
1531 lines
49 KiB
JavaScript
1531 lines
49 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/. */
|
|
|
|
this.EXPORTED_SYMBOLS = ['BookmarksEngine', "PlacesItem", "Bookmark",
|
|
"BookmarkFolder", "BookmarkQuery",
|
|
"Livemark", "BookmarkSeparator"];
|
|
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
const Cu = Components.utils;
|
|
|
|
Cu.import("resource://gre/modules/PlacesUtils.jsm");
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.import("resource://services-common/async.js");
|
|
Cu.import("resource://services-sync/constants.js");
|
|
Cu.import("resource://services-sync/engines.js");
|
|
Cu.import("resource://services-sync/record.js");
|
|
Cu.import("resource://services-sync/util.js");
|
|
Cu.import("resource://gre/modules/Task.jsm");
|
|
Cu.import("resource://gre/modules/PlacesBackups.jsm");
|
|
|
|
const ALLBOOKMARKS_ANNO = "AllBookmarks";
|
|
const DESCRIPTION_ANNO = "bookmarkProperties/description";
|
|
const SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
|
|
const MOBILEROOT_ANNO = "mobile/bookmarksRoot";
|
|
const MOBILE_ANNO = "MobileBookmarks";
|
|
const EXCLUDEBACKUP_ANNO = "places/excludeFromBackup";
|
|
const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
|
|
const PARENT_ANNO = "sync/parent";
|
|
const ORGANIZERQUERY_ANNO = "PlacesOrganizer/OrganizerQuery";
|
|
const ANNOS_TO_TRACK = [DESCRIPTION_ANNO, SIDEBAR_ANNO,
|
|
PlacesUtils.LMANNO_FEEDURI, PlacesUtils.LMANNO_SITEURI];
|
|
|
|
const SERVICE_NOT_SUPPORTED = "Service not supported on this platform";
|
|
const FOLDER_SORTINDEX = 1000000;
|
|
|
|
this.PlacesItem = function PlacesItem(collection, id, type) {
|
|
CryptoWrapper.call(this, collection, id);
|
|
this.type = type || "item";
|
|
}
|
|
PlacesItem.prototype = {
|
|
decrypt: function PlacesItem_decrypt(keyBundle) {
|
|
// Do the normal CryptoWrapper decrypt, but change types before returning
|
|
let clear = CryptoWrapper.prototype.decrypt.call(this, keyBundle);
|
|
|
|
// Convert the abstract places item to the actual object type
|
|
if (!this.deleted)
|
|
this.__proto__ = this.getTypeObject(this.type).prototype;
|
|
|
|
return clear;
|
|
},
|
|
|
|
getTypeObject: function PlacesItem_getTypeObject(type) {
|
|
switch (type) {
|
|
case "bookmark":
|
|
case "microsummary":
|
|
return Bookmark;
|
|
case "query":
|
|
return BookmarkQuery;
|
|
case "folder":
|
|
return BookmarkFolder;
|
|
case "livemark":
|
|
return Livemark;
|
|
case "separator":
|
|
return BookmarkSeparator;
|
|
case "item":
|
|
return PlacesItem;
|
|
}
|
|
throw "Unknown places item object type: " + type;
|
|
},
|
|
|
|
__proto__: CryptoWrapper.prototype,
|
|
_logName: "Sync.Record.PlacesItem",
|
|
};
|
|
|
|
Utils.deferGetSet(PlacesItem,
|
|
"cleartext",
|
|
["hasDupe", "parentid", "parentName", "type"]);
|
|
|
|
this.Bookmark = function Bookmark(collection, id, type) {
|
|
PlacesItem.call(this, collection, id, type || "bookmark");
|
|
}
|
|
Bookmark.prototype = {
|
|
__proto__: PlacesItem.prototype,
|
|
_logName: "Sync.Record.Bookmark",
|
|
};
|
|
|
|
Utils.deferGetSet(Bookmark,
|
|
"cleartext",
|
|
["title", "bmkUri", "description",
|
|
"loadInSidebar", "tags", "keyword"]);
|
|
|
|
this.BookmarkQuery = function BookmarkQuery(collection, id) {
|
|
Bookmark.call(this, collection, id, "query");
|
|
}
|
|
BookmarkQuery.prototype = {
|
|
__proto__: Bookmark.prototype,
|
|
_logName: "Sync.Record.BookmarkQuery",
|
|
};
|
|
|
|
Utils.deferGetSet(BookmarkQuery,
|
|
"cleartext",
|
|
["folderName", "queryId"]);
|
|
|
|
this.BookmarkFolder = function BookmarkFolder(collection, id, type) {
|
|
PlacesItem.call(this, collection, id, type || "folder");
|
|
}
|
|
BookmarkFolder.prototype = {
|
|
__proto__: PlacesItem.prototype,
|
|
_logName: "Sync.Record.Folder",
|
|
};
|
|
|
|
Utils.deferGetSet(BookmarkFolder, "cleartext", ["description", "title",
|
|
"children"]);
|
|
|
|
this.Livemark = function Livemark(collection, id) {
|
|
BookmarkFolder.call(this, collection, id, "livemark");
|
|
}
|
|
Livemark.prototype = {
|
|
__proto__: BookmarkFolder.prototype,
|
|
_logName: "Sync.Record.Livemark",
|
|
};
|
|
|
|
Utils.deferGetSet(Livemark, "cleartext", ["siteUri", "feedUri"]);
|
|
|
|
this.BookmarkSeparator = function BookmarkSeparator(collection, id) {
|
|
PlacesItem.call(this, collection, id, "separator");
|
|
}
|
|
BookmarkSeparator.prototype = {
|
|
__proto__: PlacesItem.prototype,
|
|
_logName: "Sync.Record.Separator",
|
|
};
|
|
|
|
Utils.deferGetSet(BookmarkSeparator, "cleartext", "pos");
|
|
|
|
|
|
let kSpecialIds = {
|
|
|
|
// Special IDs. Note that mobile can attempt to create a record on
|
|
// dereference; special accessors are provided to prevent recursion within
|
|
// observers.
|
|
guids: ["menu", "places", "tags", "toolbar", "unfiled", "mobile"],
|
|
|
|
// Create the special mobile folder to store mobile bookmarks.
|
|
createMobileRoot: function createMobileRoot() {
|
|
let root = PlacesUtils.placesRootId;
|
|
let mRoot = PlacesUtils.bookmarks.createFolder(root, "mobile", -1);
|
|
PlacesUtils.annotations.setItemAnnotation(
|
|
mRoot, MOBILEROOT_ANNO, 1, 0, PlacesUtils.annotations.EXPIRE_NEVER);
|
|
PlacesUtils.annotations.setItemAnnotation(
|
|
mRoot, EXCLUDEBACKUP_ANNO, 1, 0, PlacesUtils.annotations.EXPIRE_NEVER);
|
|
return mRoot;
|
|
},
|
|
|
|
findMobileRoot: function findMobileRoot(create) {
|
|
// Use the (one) mobile root if it already exists.
|
|
let root = PlacesUtils.annotations.getItemsWithAnnotation(MOBILEROOT_ANNO, {});
|
|
if (root.length != 0)
|
|
return root[0];
|
|
|
|
if (create)
|
|
return this.createMobileRoot();
|
|
|
|
return null;
|
|
},
|
|
|
|
// Accessors for IDs.
|
|
isSpecialGUID: function isSpecialGUID(g) {
|
|
return this.guids.indexOf(g) != -1;
|
|
},
|
|
|
|
specialIdForGUID: function specialIdForGUID(guid, create) {
|
|
if (guid == "mobile") {
|
|
return this.findMobileRoot(create);
|
|
}
|
|
return this[guid];
|
|
},
|
|
|
|
// Don't bother creating mobile: if it doesn't exist, this ID can't be it!
|
|
specialGUIDForId: function specialGUIDForId(id) {
|
|
for each (let guid in this.guids)
|
|
if (this.specialIdForGUID(guid, false) == id)
|
|
return guid;
|
|
return null;
|
|
},
|
|
|
|
get menu() PlacesUtils.bookmarksMenuFolderId,
|
|
get places() PlacesUtils.placesRootId,
|
|
get tags() PlacesUtils.tagsFolderId,
|
|
get toolbar() PlacesUtils.toolbarFolderId,
|
|
get unfiled() PlacesUtils.unfiledBookmarksFolderId,
|
|
get mobile() this.findMobileRoot(true),
|
|
};
|
|
|
|
this.BookmarksEngine = function BookmarksEngine(service) {
|
|
SyncEngine.call(this, "Bookmarks", service);
|
|
}
|
|
BookmarksEngine.prototype = {
|
|
__proto__: SyncEngine.prototype,
|
|
_recordObj: PlacesItem,
|
|
_storeObj: BookmarksStore,
|
|
_trackerObj: BookmarksTracker,
|
|
version: 2,
|
|
|
|
_sync: function _sync() {
|
|
let engine = this;
|
|
let batchEx = null;
|
|
|
|
// Try running sync in batch mode
|
|
PlacesUtils.bookmarks.runInBatchMode({
|
|
runBatched: function wrappedSync() {
|
|
try {
|
|
SyncEngine.prototype._sync.call(engine);
|
|
}
|
|
catch(ex) {
|
|
batchEx = ex;
|
|
}
|
|
}
|
|
}, null);
|
|
|
|
// Expose the exception if something inside the batch failed
|
|
if (batchEx != null) {
|
|
throw batchEx;
|
|
}
|
|
},
|
|
|
|
_guidMapFailed: false,
|
|
_buildGUIDMap: function _buildGUIDMap() {
|
|
let guidMap = {};
|
|
for (let guid in this._store.getAllIDs()) {
|
|
// Figure out with which key to store the mapping.
|
|
let key;
|
|
let id = this._store.idForGUID(guid);
|
|
switch (PlacesUtils.bookmarks.getItemType(id)) {
|
|
case PlacesUtils.bookmarks.TYPE_BOOKMARK:
|
|
|
|
// Smart bookmarks map to their annotation value.
|
|
let queryId;
|
|
try {
|
|
queryId = PlacesUtils.annotations.getItemAnnotation(
|
|
id, SMART_BOOKMARKS_ANNO);
|
|
} catch(ex) {}
|
|
|
|
if (queryId)
|
|
key = "q" + queryId;
|
|
else
|
|
key = "b" + PlacesUtils.bookmarks.getBookmarkURI(id).spec + ":" +
|
|
PlacesUtils.bookmarks.getItemTitle(id);
|
|
break;
|
|
case PlacesUtils.bookmarks.TYPE_FOLDER:
|
|
key = "f" + PlacesUtils.bookmarks.getItemTitle(id);
|
|
break;
|
|
case PlacesUtils.bookmarks.TYPE_SEPARATOR:
|
|
key = "s" + PlacesUtils.bookmarks.getItemIndex(id);
|
|
break;
|
|
default:
|
|
continue;
|
|
}
|
|
|
|
// The mapping is on a per parent-folder-name basis.
|
|
let parent = PlacesUtils.bookmarks.getFolderIdForItem(id);
|
|
if (parent <= 0)
|
|
continue;
|
|
|
|
let parentName = PlacesUtils.bookmarks.getItemTitle(parent);
|
|
if (guidMap[parentName] == null)
|
|
guidMap[parentName] = {};
|
|
|
|
// If the entry already exists, remember that there are explicit dupes.
|
|
let entry = new String(guid);
|
|
entry.hasDupe = guidMap[parentName][key] != null;
|
|
|
|
// Remember this item's GUID for its parent-name/key pair.
|
|
guidMap[parentName][key] = entry;
|
|
this._log.trace("Mapped: " + [parentName, key, entry, entry.hasDupe]);
|
|
}
|
|
|
|
return guidMap;
|
|
},
|
|
|
|
// Helper function to get a dupe GUID for an item.
|
|
_mapDupe: function _mapDupe(item) {
|
|
// Figure out if we have something to key with.
|
|
let key;
|
|
let altKey;
|
|
switch (item.type) {
|
|
case "query":
|
|
// Prior to Bug 610501, records didn't carry their Smart Bookmark
|
|
// anno, so we won't be able to dupe them correctly. This altKey
|
|
// hack should get them to dupe correctly.
|
|
if (item.queryId) {
|
|
key = "q" + item.queryId;
|
|
altKey = "b" + item.bmkUri + ":" + item.title;
|
|
break;
|
|
}
|
|
// No queryID? Fall through to the regular bookmark case.
|
|
case "bookmark":
|
|
case "microsummary":
|
|
key = "b" + item.bmkUri + ":" + item.title;
|
|
break;
|
|
case "folder":
|
|
case "livemark":
|
|
key = "f" + item.title;
|
|
break;
|
|
case "separator":
|
|
key = "s" + item.pos;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
// Figure out if we have a map to use!
|
|
// This will throw in some circumstances. That's fine.
|
|
let guidMap = this._guidMap;
|
|
|
|
// Give the GUID if we have the matching pair.
|
|
this._log.trace("Finding mapping: " + item.parentName + ", " + key);
|
|
let parent = guidMap[item.parentName];
|
|
|
|
if (!parent) {
|
|
this._log.trace("No parent => no dupe.");
|
|
return undefined;
|
|
}
|
|
|
|
let dupe = parent[key];
|
|
|
|
if (dupe) {
|
|
this._log.trace("Mapped dupe: " + dupe);
|
|
return dupe;
|
|
}
|
|
|
|
if (altKey) {
|
|
dupe = parent[altKey];
|
|
if (dupe) {
|
|
this._log.trace("Mapped dupe using altKey " + altKey + ": " + dupe);
|
|
return dupe;
|
|
}
|
|
}
|
|
|
|
this._log.trace("No dupe found for key " + key + "/" + altKey + ".");
|
|
return undefined;
|
|
},
|
|
|
|
_syncStartup: function _syncStart() {
|
|
SyncEngine.prototype._syncStartup.call(this);
|
|
|
|
let cb = Async.makeSpinningCallback();
|
|
Task.spawn(function() {
|
|
// For first-syncs, make a backup for the user to restore
|
|
if (this.lastSync == 0) {
|
|
this._log.debug("Bookmarks backup starting.");
|
|
yield PlacesBackups.create(null, true);
|
|
this._log.debug("Bookmarks backup done.");
|
|
}
|
|
}.bind(this)).then(
|
|
cb, ex => {
|
|
// Failure to create a backup is somewhat bad, but probably not bad
|
|
// enough to prevent syncing of bookmarks - so just log the error and
|
|
// continue.
|
|
this._log.warn("Got exception \"" + Utils.exceptionStr(ex) +
|
|
"\" backing up bookmarks, but continuing with sync.");
|
|
cb();
|
|
}
|
|
);
|
|
|
|
cb.wait();
|
|
|
|
this.__defineGetter__("_guidMap", function() {
|
|
// Create a mapping of folder titles and separator positions to GUID.
|
|
// We do this lazily so that we don't do any work unless we reconcile
|
|
// incoming items.
|
|
let guidMap;
|
|
try {
|
|
guidMap = this._buildGUIDMap();
|
|
} catch (ex) {
|
|
this._log.warn("Got exception \"" + Utils.exceptionStr(ex) +
|
|
"\" building GUID map." +
|
|
" Skipping all other incoming items.");
|
|
throw {code: Engine.prototype.eEngineAbortApplyIncoming,
|
|
cause: ex};
|
|
}
|
|
delete this._guidMap;
|
|
return this._guidMap = guidMap;
|
|
});
|
|
|
|
this._store._childrenToOrder = {};
|
|
},
|
|
|
|
_processIncoming: function (newitems) {
|
|
try {
|
|
SyncEngine.prototype._processIncoming.call(this, newitems);
|
|
} finally {
|
|
// Reorder children.
|
|
this._tracker.ignoreAll = true;
|
|
this._store._orderChildren();
|
|
this._tracker.ignoreAll = false;
|
|
delete this._store._childrenToOrder;
|
|
}
|
|
},
|
|
|
|
_syncFinish: function _syncFinish() {
|
|
SyncEngine.prototype._syncFinish.call(this);
|
|
this._tracker._ensureMobileQuery();
|
|
},
|
|
|
|
_syncCleanup: function _syncCleanup() {
|
|
SyncEngine.prototype._syncCleanup.call(this);
|
|
delete this._guidMap;
|
|
},
|
|
|
|
_createRecord: function _createRecord(id) {
|
|
// Create the record as usual, but mark it as having dupes if necessary.
|
|
let record = SyncEngine.prototype._createRecord.call(this, id);
|
|
let entry = this._mapDupe(record);
|
|
if (entry != null && entry.hasDupe) {
|
|
record.hasDupe = true;
|
|
}
|
|
return record;
|
|
},
|
|
|
|
_findDupe: function _findDupe(item) {
|
|
this._log.trace("Finding dupe for " + item.id +
|
|
" (already duped: " + item.hasDupe + ").");
|
|
|
|
// Don't bother finding a dupe if the incoming item has duplicates.
|
|
if (item.hasDupe) {
|
|
this._log.trace(item.id + " already a dupe: not finding one.");
|
|
return;
|
|
}
|
|
let mapped = this._mapDupe(item);
|
|
this._log.debug(item.id + " mapped to " + mapped);
|
|
return mapped;
|
|
}
|
|
};
|
|
|
|
function BookmarksStore(name, engine) {
|
|
Store.call(this, name, engine);
|
|
|
|
// Explicitly nullify our references to our cached services so we don't leak
|
|
Svc.Obs.add("places-shutdown", function() {
|
|
for each (let [query, stmt] in Iterator(this._stmts)) {
|
|
stmt.finalize();
|
|
}
|
|
this._stmts = {};
|
|
}, this);
|
|
}
|
|
BookmarksStore.prototype = {
|
|
__proto__: Store.prototype,
|
|
|
|
itemExists: function BStore_itemExists(id) {
|
|
return this.idForGUID(id, true) > 0;
|
|
},
|
|
|
|
/*
|
|
* If the record is a tag query, rewrite it to refer to the local tag ID.
|
|
*
|
|
* Otherwise, just return.
|
|
*/
|
|
preprocessTagQuery: function preprocessTagQuery(record) {
|
|
if (record.type != "query" ||
|
|
record.bmkUri == null ||
|
|
!record.folderName)
|
|
return;
|
|
|
|
// Yes, this works without chopping off the "place:" prefix.
|
|
let uri = record.bmkUri
|
|
let queriesRef = {};
|
|
let queryCountRef = {};
|
|
let optionsRef = {};
|
|
PlacesUtils.history.queryStringToQueries(uri, queriesRef, queryCountRef,
|
|
optionsRef);
|
|
|
|
// We only process tag URIs.
|
|
if (optionsRef.value.resultType != optionsRef.value.RESULTS_AS_TAG_CONTENTS)
|
|
return;
|
|
|
|
// Tag something to ensure that the tag exists.
|
|
let tag = record.folderName;
|
|
let dummyURI = Utils.makeURI("about:weave#BStore_preprocess");
|
|
PlacesUtils.tagging.tagURI(dummyURI, [tag]);
|
|
|
|
// Look for the id of the tag, which might just have been added.
|
|
let tags = this._getNode(PlacesUtils.tagsFolderId);
|
|
if (!(tags instanceof Ci.nsINavHistoryQueryResultNode)) {
|
|
this._log.debug("tags isn't an nsINavHistoryQueryResultNode; aborting.");
|
|
return;
|
|
}
|
|
|
|
tags.containerOpen = true;
|
|
try {
|
|
for (let i = 0; i < tags.childCount; i++) {
|
|
let child = tags.getChild(i);
|
|
if (child.title == tag) {
|
|
// Found the tag, so fix up the query to use the right id.
|
|
this._log.debug("Tag query folder: " + tag + " = " + child.itemId);
|
|
|
|
this._log.trace("Replacing folders in: " + uri);
|
|
for each (let q in queriesRef.value)
|
|
q.setFolders([child.itemId], 1);
|
|
|
|
record.bmkUri = PlacesUtils.history.queriesToQueryString(
|
|
queriesRef.value, queryCountRef.value, optionsRef.value);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
finally {
|
|
tags.containerOpen = false;
|
|
}
|
|
},
|
|
|
|
applyIncoming: function BStore_applyIncoming(record) {
|
|
this._log.debug("Applying record " + record.id);
|
|
let isSpecial = record.id in kSpecialIds;
|
|
|
|
if (record.deleted) {
|
|
if (isSpecial) {
|
|
this._log.warn("Ignoring deletion for special record " + record.id);
|
|
return;
|
|
}
|
|
|
|
// Don't bother with pre and post-processing for deletions.
|
|
Store.prototype.applyIncoming.call(this, record);
|
|
return;
|
|
}
|
|
|
|
// For special folders we're only interested in child ordering.
|
|
if (isSpecial && record.children) {
|
|
this._log.debug("Processing special node: " + record.id);
|
|
// Reorder children later
|
|
this._childrenToOrder[record.id] = record.children;
|
|
return;
|
|
}
|
|
|
|
// Skip malformed records. (Bug 806460.)
|
|
if (record.type == "query" &&
|
|
!record.bmkUri) {
|
|
this._log.warn("Skipping malformed query bookmark: " + record.id);
|
|
return;
|
|
}
|
|
|
|
// Preprocess the record before doing the normal apply.
|
|
this.preprocessTagQuery(record);
|
|
|
|
// Figure out the local id of the parent GUID if available
|
|
let parentGUID = record.parentid;
|
|
if (!parentGUID) {
|
|
throw "Record " + record.id + " has invalid parentid: " + parentGUID;
|
|
}
|
|
this._log.debug("Local parent is " + parentGUID);
|
|
|
|
let parentId = this.idForGUID(parentGUID);
|
|
if (parentId > 0) {
|
|
// Save the parent id for modifying the bookmark later
|
|
record._parent = parentId;
|
|
record._orphan = false;
|
|
this._log.debug("Record " + record.id + " is not an orphan.");
|
|
} else {
|
|
this._log.trace("Record " + record.id +
|
|
" is an orphan: could not find parent " + parentGUID);
|
|
record._orphan = true;
|
|
}
|
|
|
|
// Do the normal processing of incoming records
|
|
Store.prototype.applyIncoming.call(this, record);
|
|
|
|
// Do some post-processing if we have an item
|
|
let itemId = this.idForGUID(record.id);
|
|
if (itemId > 0) {
|
|
// Move any children that are looking for this folder as a parent
|
|
if (record.type == "folder") {
|
|
this._reparentOrphans(itemId);
|
|
// Reorder children later
|
|
if (record.children)
|
|
this._childrenToOrder[record.id] = record.children;
|
|
}
|
|
|
|
// Create an annotation to remember that it needs reparenting.
|
|
if (record._orphan) {
|
|
PlacesUtils.annotations.setItemAnnotation(
|
|
itemId, PARENT_ANNO, parentGUID, 0,
|
|
PlacesUtils.annotations.EXPIRE_NEVER);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Find all ids of items that have a given value for an annotation
|
|
*/
|
|
_findAnnoItems: function BStore__findAnnoItems(anno, val) {
|
|
return PlacesUtils.annotations.getItemsWithAnnotation(anno, {})
|
|
.filter(function(id) {
|
|
return PlacesUtils.annotations.getItemAnnotation(id, anno) == val;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* For the provided parent item, attach its children to it
|
|
*/
|
|
_reparentOrphans: function _reparentOrphans(parentId) {
|
|
// Find orphans and reunite with this folder parent
|
|
let parentGUID = this.GUIDForId(parentId);
|
|
let orphans = this._findAnnoItems(PARENT_ANNO, parentGUID);
|
|
|
|
this._log.debug("Reparenting orphans " + orphans + " to " + parentId);
|
|
orphans.forEach(function(orphan) {
|
|
// Move the orphan to the parent and drop the missing parent annotation
|
|
if (this._reparentItem(orphan, parentId)) {
|
|
PlacesUtils.annotations.removeItemAnnotation(orphan, PARENT_ANNO);
|
|
}
|
|
}, this);
|
|
},
|
|
|
|
_reparentItem: function _reparentItem(itemId, parentId) {
|
|
this._log.trace("Attempting to move item " + itemId + " to new parent " +
|
|
parentId);
|
|
try {
|
|
if (parentId > 0) {
|
|
PlacesUtils.bookmarks.moveItem(itemId, parentId,
|
|
PlacesUtils.bookmarks.DEFAULT_INDEX);
|
|
return true;
|
|
}
|
|
} catch(ex) {
|
|
this._log.debug("Failed to reparent item. " + Utils.exceptionStr(ex));
|
|
}
|
|
return false;
|
|
},
|
|
|
|
// Turn a record's nsINavBookmarksService constant and other attributes into
|
|
// a granular type for comparison.
|
|
_recordType: function _recordType(itemId) {
|
|
let bms = PlacesUtils.bookmarks;
|
|
let type = bms.getItemType(itemId);
|
|
|
|
switch (type) {
|
|
case bms.TYPE_FOLDER:
|
|
if (PlacesUtils.annotations
|
|
.itemHasAnnotation(itemId, PlacesUtils.LMANNO_FEEDURI)) {
|
|
return "livemark";
|
|
}
|
|
return "folder";
|
|
|
|
case bms.TYPE_BOOKMARK:
|
|
let bmkUri = bms.getBookmarkURI(itemId).spec;
|
|
if (bmkUri.indexOf("place:") == 0) {
|
|
return "query";
|
|
}
|
|
return "bookmark";
|
|
|
|
case bms.TYPE_SEPARATOR:
|
|
return "separator";
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
},
|
|
|
|
create: function BStore_create(record) {
|
|
// Default to unfiled if we don't have the parent yet.
|
|
|
|
// Valid parent IDs are all positive integers. Other values -- undefined,
|
|
// null, -1 -- all compare false for > 0, so this catches them all. We
|
|
// don't just use <= without the !, because undefined and null compare
|
|
// false for that, too!
|
|
if (!(record._parent > 0)) {
|
|
this._log.debug("Parent is " + record._parent + "; reparenting to unfiled.");
|
|
record._parent = kSpecialIds.unfiled;
|
|
}
|
|
|
|
let newId;
|
|
switch (record.type) {
|
|
case "bookmark":
|
|
case "query":
|
|
case "microsummary": {
|
|
let uri = Utils.makeURI(record.bmkUri);
|
|
newId = PlacesUtils.bookmarks.insertBookmark(
|
|
record._parent, uri, PlacesUtils.bookmarks.DEFAULT_INDEX, record.title);
|
|
this._log.debug("created bookmark " + newId + " under " + record._parent
|
|
+ " as " + record.title + " " + record.bmkUri);
|
|
|
|
// Smart bookmark annotations are strings.
|
|
if (record.queryId) {
|
|
PlacesUtils.annotations.setItemAnnotation(
|
|
newId, SMART_BOOKMARKS_ANNO, record.queryId, 0,
|
|
PlacesUtils.annotations.EXPIRE_NEVER);
|
|
}
|
|
|
|
if (Array.isArray(record.tags)) {
|
|
this._tagURI(uri, record.tags);
|
|
}
|
|
PlacesUtils.bookmarks.setKeywordForBookmark(newId, record.keyword);
|
|
if (record.description) {
|
|
PlacesUtils.annotations.setItemAnnotation(
|
|
newId, DESCRIPTION_ANNO, record.description, 0,
|
|
PlacesUtils.annotations.EXPIRE_NEVER);
|
|
}
|
|
|
|
if (record.loadInSidebar) {
|
|
PlacesUtils.annotations.setItemAnnotation(
|
|
newId, SIDEBAR_ANNO, true, 0,
|
|
PlacesUtils.annotations.EXPIRE_NEVER);
|
|
}
|
|
|
|
} break;
|
|
case "folder":
|
|
newId = PlacesUtils.bookmarks.createFolder(
|
|
record._parent, record.title, PlacesUtils.bookmarks.DEFAULT_INDEX);
|
|
this._log.debug("created folder " + newId + " under " + record._parent
|
|
+ " as " + record.title);
|
|
|
|
if (record.description) {
|
|
PlacesUtils.annotations.setItemAnnotation(
|
|
newId, DESCRIPTION_ANNO, record.description, 0,
|
|
PlacesUtils.annotations.EXPIRE_NEVER);
|
|
}
|
|
|
|
// record.children will be dealt with in _orderChildren.
|
|
break;
|
|
case "livemark":
|
|
let siteURI = null;
|
|
if (!record.feedUri) {
|
|
this._log.debug("No feed URI: skipping livemark record " + record.id);
|
|
return;
|
|
}
|
|
if (PlacesUtils.annotations
|
|
.itemHasAnnotation(record._parent, PlacesUtils.LMANNO_FEEDURI)) {
|
|
this._log.debug("Invalid parent: skipping livemark record " + record.id);
|
|
return;
|
|
}
|
|
|
|
if (record.siteUri != null)
|
|
siteURI = Utils.makeURI(record.siteUri);
|
|
|
|
// Until this engine can handle asynchronous error reporting, we need to
|
|
// detect errors on creation synchronously.
|
|
let spinningCb = Async.makeSpinningCallback();
|
|
|
|
let livemarkObj = {title: record.title,
|
|
parentId: record._parent,
|
|
index: PlacesUtils.bookmarks.DEFAULT_INDEX,
|
|
feedURI: Utils.makeURI(record.feedUri),
|
|
siteURI: siteURI,
|
|
guid: record.id};
|
|
PlacesUtils.livemarks.addLivemark(livemarkObj).then(
|
|
aLivemark => { spinningCb(null, [Components.results.NS_OK, aLivemark]) },
|
|
() => { spinningCb(null, [Components.results.NS_ERROR_UNEXPECTED, aLivemark]) }
|
|
);
|
|
|
|
let [status, livemark] = spinningCb.wait();
|
|
if (!Components.isSuccessCode(status)) {
|
|
throw status;
|
|
}
|
|
|
|
this._log.debug("Created livemark " + livemark.id + " under " +
|
|
livemark.parentId + " as " + livemark.title +
|
|
", " + livemark.siteURI.spec + ", " +
|
|
livemark.feedURI.spec + ", GUID " +
|
|
livemark.guid);
|
|
break;
|
|
case "separator":
|
|
newId = PlacesUtils.bookmarks.insertSeparator(
|
|
record._parent, PlacesUtils.bookmarks.DEFAULT_INDEX);
|
|
this._log.debug("created separator " + newId + " under " + record._parent);
|
|
break;
|
|
case "item":
|
|
this._log.debug(" -> got a generic places item.. do nothing?");
|
|
return;
|
|
default:
|
|
this._log.error("_create: Unknown item type: " + record.type);
|
|
return;
|
|
}
|
|
|
|
if (newId) {
|
|
// Livemarks can set the GUID through the API, so there's no need to
|
|
// do that here.
|
|
this._log.trace("Setting GUID of new item " + newId + " to " + record.id);
|
|
this._setGUID(newId, record.id);
|
|
}
|
|
},
|
|
|
|
// Factored out of `remove` to avoid redundant DB queries when the Places ID
|
|
// is already known.
|
|
removeById: function removeById(itemId, guid) {
|
|
let type = PlacesUtils.bookmarks.getItemType(itemId);
|
|
|
|
switch (type) {
|
|
case PlacesUtils.bookmarks.TYPE_BOOKMARK:
|
|
this._log.debug(" -> removing bookmark " + guid);
|
|
PlacesUtils.bookmarks.removeItem(itemId);
|
|
break;
|
|
case PlacesUtils.bookmarks.TYPE_FOLDER:
|
|
this._log.debug(" -> removing folder " + guid);
|
|
PlacesUtils.bookmarks.removeItem(itemId);
|
|
break;
|
|
case PlacesUtils.bookmarks.TYPE_SEPARATOR:
|
|
this._log.debug(" -> removing separator " + guid);
|
|
PlacesUtils.bookmarks.removeItem(itemId);
|
|
break;
|
|
default:
|
|
this._log.error("remove: Unknown item type: " + type);
|
|
break;
|
|
}
|
|
},
|
|
|
|
remove: function BStore_remove(record) {
|
|
if (kSpecialIds.isSpecialGUID(record.id)) {
|
|
this._log.warn("Refusing to remove special folder " + record.id);
|
|
return;
|
|
}
|
|
|
|
let itemId = this.idForGUID(record.id);
|
|
if (itemId <= 0) {
|
|
this._log.debug("Item " + record.id + " already removed");
|
|
return;
|
|
}
|
|
this.removeById(itemId, record.id);
|
|
},
|
|
|
|
_taggableTypes: ["bookmark", "microsummary", "query"],
|
|
isTaggable: function isTaggable(recordType) {
|
|
return this._taggableTypes.indexOf(recordType) != -1;
|
|
},
|
|
|
|
update: function BStore_update(record) {
|
|
let itemId = this.idForGUID(record.id);
|
|
|
|
if (itemId <= 0) {
|
|
this._log.debug("Skipping update for unknown item: " + record.id);
|
|
return;
|
|
}
|
|
|
|
// Two items are the same type if they have the same ItemType in Places,
|
|
// and also share some key characteristics (e.g., both being livemarks).
|
|
// We figure this out by examining the item to find the equivalent granular
|
|
// (string) type.
|
|
// If they're not the same type, we can't just update attributes. Delete
|
|
// then recreate the record instead.
|
|
let localItemType = this._recordType(itemId);
|
|
let remoteRecordType = record.type;
|
|
this._log.trace("Local type: " + localItemType + ". " +
|
|
"Remote type: " + remoteRecordType + ".");
|
|
|
|
if (localItemType != remoteRecordType) {
|
|
this._log.debug("Local record and remote record differ in type. " +
|
|
"Deleting and recreating.");
|
|
this.removeById(itemId, record.id);
|
|
this.create(record);
|
|
return;
|
|
}
|
|
|
|
this._log.trace("Updating " + record.id + " (" + itemId + ")");
|
|
|
|
// Move the bookmark to a new parent or new position if necessary
|
|
if (record._parent > 0 &&
|
|
PlacesUtils.bookmarks.getFolderIdForItem(itemId) != record._parent) {
|
|
this._reparentItem(itemId, record._parent);
|
|
}
|
|
|
|
for (let [key, val] in Iterator(record.cleartext)) {
|
|
switch (key) {
|
|
case "title":
|
|
PlacesUtils.bookmarks.setItemTitle(itemId, val);
|
|
break;
|
|
case "bmkUri":
|
|
PlacesUtils.bookmarks.changeBookmarkURI(itemId, Utils.makeURI(val));
|
|
break;
|
|
case "tags":
|
|
if (Array.isArray(val)) {
|
|
if (this.isTaggable(remoteRecordType)) {
|
|
this._tagID(itemId, val);
|
|
} else {
|
|
this._log.debug("Remote record type is invalid for tags: " + remoteRecordType);
|
|
}
|
|
}
|
|
break;
|
|
case "keyword":
|
|
PlacesUtils.bookmarks.setKeywordForBookmark(itemId, val);
|
|
break;
|
|
case "description":
|
|
if (val) {
|
|
PlacesUtils.annotations.setItemAnnotation(
|
|
itemId, DESCRIPTION_ANNO, val, 0,
|
|
PlacesUtils.annotations.EXPIRE_NEVER);
|
|
} else {
|
|
PlacesUtils.annotations.removeItemAnnotation(itemId, DESCRIPTION_ANNO);
|
|
}
|
|
break;
|
|
case "loadInSidebar":
|
|
if (val) {
|
|
PlacesUtils.annotations.setItemAnnotation(
|
|
itemId, SIDEBAR_ANNO, true, 0,
|
|
PlacesUtils.annotations.EXPIRE_NEVER);
|
|
} else {
|
|
PlacesUtils.annotations.removeItemAnnotation(itemId, SIDEBAR_ANNO);
|
|
}
|
|
break;
|
|
case "queryId":
|
|
PlacesUtils.annotations.setItemAnnotation(
|
|
itemId, SMART_BOOKMARKS_ANNO, val, 0,
|
|
PlacesUtils.annotations.EXPIRE_NEVER);
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
_orderChildren: function _orderChildren() {
|
|
for (let [guid, children] in Iterator(this._childrenToOrder)) {
|
|
// Reorder children according to the GUID list. Gracefully deal
|
|
// with missing items, e.g. locally deleted.
|
|
let delta = 0;
|
|
let parent = null;
|
|
for (let idx = 0; idx < children.length; idx++) {
|
|
let itemid = this.idForGUID(children[idx]);
|
|
if (itemid == -1) {
|
|
delta += 1;
|
|
this._log.trace("Could not locate record " + children[idx]);
|
|
continue;
|
|
}
|
|
try {
|
|
// This code path could be optimized by caching the parent earlier.
|
|
// Doing so should take in count any edge case due to reparenting
|
|
// or parent invalidations though.
|
|
if (!parent) {
|
|
parent = PlacesUtils.bookmarks.getFolderIdForItem(itemid);
|
|
}
|
|
PlacesUtils.bookmarks.moveItem(itemid, parent, idx - delta);
|
|
} catch (ex) {
|
|
this._log.debug("Could not move item " + children[idx] + ": " + ex);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
changeItemID: function BStore_changeItemID(oldID, newID) {
|
|
this._log.debug("Changing GUID " + oldID + " to " + newID);
|
|
|
|
// Make sure there's an item to change GUIDs
|
|
let itemId = this.idForGUID(oldID);
|
|
if (itemId <= 0)
|
|
return;
|
|
|
|
this._setGUID(itemId, newID);
|
|
},
|
|
|
|
_getNode: function BStore__getNode(folder) {
|
|
let query = PlacesUtils.history.getNewQuery();
|
|
query.setFolders([folder], 1);
|
|
return PlacesUtils.history.executeQuery(
|
|
query, PlacesUtils.history.getNewQueryOptions()).root;
|
|
},
|
|
|
|
_getTags: function BStore__getTags(uri) {
|
|
try {
|
|
if (typeof(uri) == "string")
|
|
uri = Utils.makeURI(uri);
|
|
} catch(e) {
|
|
this._log.warn("Could not parse URI \"" + uri + "\": " + e);
|
|
}
|
|
return PlacesUtils.tagging.getTagsForURI(uri, {});
|
|
},
|
|
|
|
_getDescription: function BStore__getDescription(id) {
|
|
try {
|
|
return PlacesUtils.annotations.getItemAnnotation(id, DESCRIPTION_ANNO);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
},
|
|
|
|
_isLoadInSidebar: function BStore__isLoadInSidebar(id) {
|
|
return PlacesUtils.annotations.itemHasAnnotation(id, SIDEBAR_ANNO);
|
|
},
|
|
|
|
get _childGUIDsStm() {
|
|
return this._getStmt(
|
|
"SELECT id AS item_id, guid " +
|
|
"FROM moz_bookmarks " +
|
|
"WHERE parent = :parent " +
|
|
"ORDER BY position");
|
|
},
|
|
_childGUIDsCols: ["item_id", "guid"],
|
|
|
|
_getChildGUIDsForId: function _getChildGUIDsForId(itemid) {
|
|
let stmt = this._childGUIDsStm;
|
|
stmt.params.parent = itemid;
|
|
let rows = Async.querySpinningly(stmt, this._childGUIDsCols);
|
|
return rows.map(function (row) {
|
|
if (row.guid) {
|
|
return row.guid;
|
|
}
|
|
// A GUID hasn't been assigned to this item yet, do this now.
|
|
return this.GUIDForId(row.item_id);
|
|
}, this);
|
|
},
|
|
|
|
// Create a record starting from the weave id (places guid)
|
|
createRecord: function createRecord(id, collection) {
|
|
let placeId = this.idForGUID(id);
|
|
let record;
|
|
if (placeId <= 0) { // deleted item
|
|
record = new PlacesItem(collection, id);
|
|
record.deleted = true;
|
|
return record;
|
|
}
|
|
|
|
let parent = PlacesUtils.bookmarks.getFolderIdForItem(placeId);
|
|
switch (PlacesUtils.bookmarks.getItemType(placeId)) {
|
|
case PlacesUtils.bookmarks.TYPE_BOOKMARK:
|
|
let bmkUri = PlacesUtils.bookmarks.getBookmarkURI(placeId).spec;
|
|
if (bmkUri.indexOf("place:") == 0) {
|
|
record = new BookmarkQuery(collection, id);
|
|
|
|
// Get the actual tag name instead of the local itemId
|
|
let folder = bmkUri.match(/[:&]folder=(\d+)/);
|
|
try {
|
|
// There might not be the tag yet when creating on a new client
|
|
if (folder != null) {
|
|
folder = folder[1];
|
|
record.folderName = PlacesUtils.bookmarks.getItemTitle(folder);
|
|
this._log.trace("query id: " + folder + " = " + record.folderName);
|
|
}
|
|
}
|
|
catch(ex) {}
|
|
|
|
// Persist the Smart Bookmark anno, if found.
|
|
try {
|
|
let anno = PlacesUtils.annotations.getItemAnnotation(placeId, SMART_BOOKMARKS_ANNO);
|
|
if (anno != null) {
|
|
this._log.trace("query anno: " + SMART_BOOKMARKS_ANNO +
|
|
" = " + anno);
|
|
record.queryId = anno;
|
|
}
|
|
}
|
|
catch(ex) {}
|
|
}
|
|
else {
|
|
record = new Bookmark(collection, id);
|
|
}
|
|
record.title = PlacesUtils.bookmarks.getItemTitle(placeId);
|
|
|
|
record.parentName = PlacesUtils.bookmarks.getItemTitle(parent);
|
|
record.bmkUri = bmkUri;
|
|
record.tags = this._getTags(record.bmkUri);
|
|
record.keyword = PlacesUtils.bookmarks.getKeywordForBookmark(placeId);
|
|
record.description = this._getDescription(placeId);
|
|
record.loadInSidebar = this._isLoadInSidebar(placeId);
|
|
break;
|
|
|
|
case PlacesUtils.bookmarks.TYPE_FOLDER:
|
|
if (PlacesUtils.annotations
|
|
.itemHasAnnotation(placeId, PlacesUtils.LMANNO_FEEDURI)) {
|
|
record = new Livemark(collection, id);
|
|
let as = PlacesUtils.annotations;
|
|
record.feedUri = as.getItemAnnotation(placeId, PlacesUtils.LMANNO_FEEDURI);
|
|
try {
|
|
record.siteUri = as.getItemAnnotation(placeId, PlacesUtils.LMANNO_SITEURI);
|
|
} catch (ex) {}
|
|
} else {
|
|
record = new BookmarkFolder(collection, id);
|
|
}
|
|
|
|
if (parent > 0)
|
|
record.parentName = PlacesUtils.bookmarks.getItemTitle(parent);
|
|
record.title = PlacesUtils.bookmarks.getItemTitle(placeId);
|
|
record.description = this._getDescription(placeId);
|
|
record.children = this._getChildGUIDsForId(placeId);
|
|
break;
|
|
|
|
case PlacesUtils.bookmarks.TYPE_SEPARATOR:
|
|
record = new BookmarkSeparator(collection, id);
|
|
if (parent > 0)
|
|
record.parentName = PlacesUtils.bookmarks.getItemTitle(parent);
|
|
// Create a positioning identifier for the separator, used by _mapDupe
|
|
record.pos = PlacesUtils.bookmarks.getItemIndex(placeId);
|
|
break;
|
|
|
|
default:
|
|
record = new PlacesItem(collection, id);
|
|
this._log.warn("Unknown item type, cannot serialize: " +
|
|
PlacesUtils.bookmarks.getItemType(placeId));
|
|
}
|
|
|
|
record.parentid = this.GUIDForId(parent);
|
|
record.sortindex = this._calculateIndex(record);
|
|
|
|
return record;
|
|
},
|
|
|
|
_stmts: {},
|
|
_getStmt: function(query) {
|
|
if (query in this._stmts) {
|
|
return this._stmts[query];
|
|
}
|
|
|
|
this._log.trace("Creating SQL statement: " + query);
|
|
let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
|
|
.DBConnection;
|
|
return this._stmts[query] = db.createAsyncStatement(query);
|
|
},
|
|
|
|
get _frecencyStm() {
|
|
return this._getStmt(
|
|
"SELECT frecency " +
|
|
"FROM moz_places " +
|
|
"WHERE url = :url " +
|
|
"LIMIT 1");
|
|
},
|
|
_frecencyCols: ["frecency"],
|
|
|
|
get _setGUIDStm() {
|
|
return this._getStmt(
|
|
"UPDATE moz_bookmarks " +
|
|
"SET guid = :guid " +
|
|
"WHERE id = :item_id");
|
|
},
|
|
|
|
// Some helper functions to handle GUIDs
|
|
_setGUID: function _setGUID(id, guid) {
|
|
if (!guid)
|
|
guid = Utils.makeGUID();
|
|
|
|
let stmt = this._setGUIDStm;
|
|
stmt.params.guid = guid;
|
|
stmt.params.item_id = id;
|
|
Async.querySpinningly(stmt);
|
|
return guid;
|
|
},
|
|
|
|
get _guidForIdStm() {
|
|
return this._getStmt(
|
|
"SELECT guid " +
|
|
"FROM moz_bookmarks " +
|
|
"WHERE id = :item_id");
|
|
},
|
|
_guidForIdCols: ["guid"],
|
|
|
|
GUIDForId: function GUIDForId(id) {
|
|
let special = kSpecialIds.specialGUIDForId(id);
|
|
if (special)
|
|
return special;
|
|
|
|
let stmt = this._guidForIdStm;
|
|
stmt.params.item_id = id;
|
|
|
|
// Use the existing GUID if it exists
|
|
let result = Async.querySpinningly(stmt, this._guidForIdCols)[0];
|
|
if (result && result.guid)
|
|
return result.guid;
|
|
|
|
// Give the uri a GUID if it doesn't have one
|
|
return this._setGUID(id);
|
|
},
|
|
|
|
get _idForGUIDStm() {
|
|
return this._getStmt(
|
|
"SELECT id AS item_id " +
|
|
"FROM moz_bookmarks " +
|
|
"WHERE guid = :guid");
|
|
},
|
|
_idForGUIDCols: ["item_id"],
|
|
|
|
// noCreate is provided as an optional argument to prevent the creation of
|
|
// non-existent special records, such as "mobile".
|
|
idForGUID: function idForGUID(guid, noCreate) {
|
|
if (kSpecialIds.isSpecialGUID(guid))
|
|
return kSpecialIds.specialIdForGUID(guid, !noCreate);
|
|
|
|
let stmt = this._idForGUIDStm;
|
|
// guid might be a String object rather than a string.
|
|
stmt.params.guid = guid.toString();
|
|
|
|
let results = Async.querySpinningly(stmt, this._idForGUIDCols);
|
|
this._log.trace("Number of rows matching GUID " + guid + ": "
|
|
+ results.length);
|
|
|
|
// Here's the one we care about: the first.
|
|
let result = results[0];
|
|
|
|
if (!result)
|
|
return -1;
|
|
|
|
return result.item_id;
|
|
},
|
|
|
|
_calculateIndex: function _calculateIndex(record) {
|
|
// Ensure folders have a very high sort index so they're not synced last.
|
|
if (record.type == "folder")
|
|
return FOLDER_SORTINDEX;
|
|
|
|
// For anything directly under the toolbar, give it a boost of more than an
|
|
// unvisited bookmark
|
|
let index = 0;
|
|
if (record.parentid == "toolbar")
|
|
index += 150;
|
|
|
|
// Add in the bookmark's frecency if we have something.
|
|
if (record.bmkUri != null) {
|
|
this._frecencyStm.params.url = record.bmkUri;
|
|
let result = Async.querySpinningly(this._frecencyStm, this._frecencyCols);
|
|
if (result.length)
|
|
index += result[0].frecency;
|
|
}
|
|
|
|
return index;
|
|
},
|
|
|
|
_getChildren: function BStore_getChildren(guid, items) {
|
|
let node = guid; // the recursion case
|
|
if (typeof(node) == "string") { // callers will give us the guid as the first arg
|
|
let nodeID = this.idForGUID(guid, true);
|
|
if (!nodeID) {
|
|
this._log.debug("No node for GUID " + guid + "; returning no children.");
|
|
return items;
|
|
}
|
|
node = this._getNode(nodeID);
|
|
}
|
|
|
|
if (node.type == node.RESULT_TYPE_FOLDER) {
|
|
node.QueryInterface(Ci.nsINavHistoryQueryResultNode);
|
|
node.containerOpen = true;
|
|
try {
|
|
// Remember all the children GUIDs and recursively get more
|
|
for (let i = 0; i < node.childCount; i++) {
|
|
let child = node.getChild(i);
|
|
items[this.GUIDForId(child.itemId)] = true;
|
|
this._getChildren(child, items);
|
|
}
|
|
}
|
|
finally {
|
|
node.containerOpen = false;
|
|
}
|
|
}
|
|
|
|
return items;
|
|
},
|
|
|
|
/**
|
|
* Associates the URI of the item with the provided ID with the
|
|
* provided array of tags.
|
|
* If the provided ID does not identify an item with a URI,
|
|
* returns immediately.
|
|
*/
|
|
_tagID: function _tagID(itemID, tags) {
|
|
if (!itemID || !tags) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let u = PlacesUtils.bookmarks.getBookmarkURI(itemID);
|
|
this._tagURI(u, tags);
|
|
} catch (e) {
|
|
this._log.warn("Got exception fetching URI for " + itemID + ": not tagging. " +
|
|
Utils.exceptionStr(e));
|
|
|
|
// I guess it doesn't have a URI. Don't try to tag it.
|
|
return;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Associate the provided URI with the provided array of tags.
|
|
* If the provided URI is falsy, returns immediately.
|
|
*/
|
|
_tagURI: function _tagURI(bookmarkURI, tags) {
|
|
if (!bookmarkURI || !tags) {
|
|
return;
|
|
}
|
|
|
|
// Filter out any null/undefined/empty tags.
|
|
tags = tags.filter(function(t) t);
|
|
|
|
// Temporarily tag a dummy URI to preserve tag ids when untagging.
|
|
let dummyURI = Utils.makeURI("about:weave#BStore_tagURI");
|
|
PlacesUtils.tagging.tagURI(dummyURI, tags);
|
|
PlacesUtils.tagging.untagURI(bookmarkURI, null);
|
|
PlacesUtils.tagging.tagURI(bookmarkURI, tags);
|
|
PlacesUtils.tagging.untagURI(dummyURI, null);
|
|
},
|
|
|
|
getAllIDs: function BStore_getAllIDs() {
|
|
let items = {"menu": true,
|
|
"toolbar": true};
|
|
for each (let guid in kSpecialIds.guids) {
|
|
if (guid != "places" && guid != "tags")
|
|
this._getChildren(guid, items);
|
|
}
|
|
return items;
|
|
},
|
|
|
|
wipe: function BStore_wipe() {
|
|
let cb = Async.makeSpinningCallback();
|
|
Task.spawn(function() {
|
|
// Save a backup before clearing out all bookmarks.
|
|
yield PlacesBackups.create(null, true);
|
|
for each (let guid in kSpecialIds.guids)
|
|
if (guid != "places") {
|
|
let id = kSpecialIds.specialIdForGUID(guid);
|
|
if (id)
|
|
PlacesUtils.bookmarks.removeFolderChildren(id);
|
|
}
|
|
cb();
|
|
});
|
|
cb.wait();
|
|
}
|
|
};
|
|
|
|
function BookmarksTracker(name, engine) {
|
|
Tracker.call(this, name, engine);
|
|
|
|
Svc.Obs.add("places-shutdown", this);
|
|
}
|
|
BookmarksTracker.prototype = {
|
|
__proto__: Tracker.prototype,
|
|
|
|
startTracking: function() {
|
|
PlacesUtils.bookmarks.addObserver(this, true);
|
|
Svc.Obs.add("bookmarks-restore-begin", this);
|
|
Svc.Obs.add("bookmarks-restore-success", this);
|
|
Svc.Obs.add("bookmarks-restore-failed", this);
|
|
},
|
|
|
|
stopTracking: function() {
|
|
PlacesUtils.bookmarks.removeObserver(this);
|
|
Svc.Obs.remove("bookmarks-restore-begin", this);
|
|
Svc.Obs.remove("bookmarks-restore-success", this);
|
|
Svc.Obs.remove("bookmarks-restore-failed", this);
|
|
},
|
|
|
|
observe: function observe(subject, topic, data) {
|
|
Tracker.prototype.observe.call(this, subject, topic, data);
|
|
|
|
switch (topic) {
|
|
case "bookmarks-restore-begin":
|
|
this._log.debug("Ignoring changes from importing bookmarks.");
|
|
this.ignoreAll = true;
|
|
break;
|
|
case "bookmarks-restore-success":
|
|
this._log.debug("Tracking all items on successful import.");
|
|
this.ignoreAll = false;
|
|
|
|
this._log.debug("Restore succeeded: wiping server and other clients.");
|
|
this.engine.service.resetClient([this.name]);
|
|
this.engine.service.wipeServer([this.name]);
|
|
this.engine.service.clientsEngine.sendCommand("wipeEngine", [this.name]);
|
|
break;
|
|
case "bookmarks-restore-failed":
|
|
this._log.debug("Tracking all items on failed import.");
|
|
this.ignoreAll = false;
|
|
break;
|
|
}
|
|
},
|
|
|
|
QueryInterface: XPCOMUtils.generateQI([
|
|
Ci.nsINavBookmarkObserver,
|
|
Ci.nsINavBookmarkObserver_MOZILLA_1_9_1_ADDITIONS,
|
|
Ci.nsISupportsWeakReference
|
|
]),
|
|
|
|
/**
|
|
* Add a bookmark GUID to be uploaded and bump up the sync score.
|
|
*
|
|
* @param itemGuid
|
|
* GUID of the bookmark to upload.
|
|
*/
|
|
_add: function BMT__add(itemId, guid) {
|
|
guid = kSpecialIds.specialGUIDForId(itemId) || guid;
|
|
if (this.addChangedID(guid))
|
|
this._upScore();
|
|
},
|
|
|
|
/* Every add/remove/change will trigger a sync for MULTI_DEVICE. */
|
|
_upScore: function BMT__upScore() {
|
|
this.score += SCORE_INCREMENT_XLARGE;
|
|
},
|
|
|
|
/**
|
|
* Determine if a change should be ignored.
|
|
*
|
|
* @param itemId
|
|
* Item under consideration to ignore
|
|
* @param folder (optional)
|
|
* Folder of the item being changed
|
|
*/
|
|
_ignore: function BMT__ignore(itemId, folder, guid) {
|
|
// Ignore unconditionally if the engine tells us to.
|
|
if (this.ignoreAll)
|
|
return true;
|
|
|
|
// Get the folder id if we weren't given one.
|
|
if (folder == null) {
|
|
try {
|
|
folder = PlacesUtils.bookmarks.getFolderIdForItem(itemId);
|
|
} catch (ex) {
|
|
this._log.debug("getFolderIdForItem(" + itemId +
|
|
") threw; calling _ensureMobileQuery.");
|
|
// I'm guessing that gFIFI can throw, and perhaps that's why
|
|
// _ensureMobileQuery is here at all. Try not to call it.
|
|
this._ensureMobileQuery();
|
|
folder = PlacesUtils.bookmarks.getFolderIdForItem(itemId);
|
|
}
|
|
}
|
|
|
|
// Ignore changes to tags (folders under the tags folder).
|
|
let tags = kSpecialIds.tags;
|
|
if (folder == tags)
|
|
return true;
|
|
|
|
// Ignore tag items (the actual instance of a tag for a bookmark).
|
|
if (PlacesUtils.bookmarks.getFolderIdForItem(folder) == tags)
|
|
return true;
|
|
|
|
// Make sure to remove items that have the exclude annotation.
|
|
if (PlacesUtils.annotations.itemHasAnnotation(itemId, EXCLUDEBACKUP_ANNO)) {
|
|
this.removeChangedID(guid);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
onItemAdded: function BMT_onItemAdded(itemId, folder, index,
|
|
itemType, uri, title, dateAdded,
|
|
guid, parentGuid) {
|
|
if (this._ignore(itemId, folder, guid))
|
|
return;
|
|
|
|
this._log.trace("onItemAdded: " + itemId);
|
|
this._add(itemId, guid);
|
|
this._add(folder, parentGuid);
|
|
},
|
|
|
|
onItemRemoved: function (itemId, parentId, index, type, uri,
|
|
guid, parentGuid) {
|
|
if (this._ignore(itemId, parentId, guid)) {
|
|
return;
|
|
}
|
|
|
|
this._log.trace("onItemRemoved: " + itemId);
|
|
this._add(itemId, guid);
|
|
this._add(parentId, parentGuid);
|
|
},
|
|
|
|
_ensureMobileQuery: function _ensureMobileQuery() {
|
|
let find = function (val)
|
|
PlacesUtils.annotations.getItemsWithAnnotation(ORGANIZERQUERY_ANNO, {}).filter(
|
|
function (id) PlacesUtils.annotations.getItemAnnotation(id, ORGANIZERQUERY_ANNO) == val
|
|
);
|
|
|
|
// Don't continue if the Library isn't ready
|
|
let all = find(ALLBOOKMARKS_ANNO);
|
|
if (all.length == 0)
|
|
return;
|
|
|
|
// Disable handling of notifications while changing the mobile query
|
|
this.ignoreAll = true;
|
|
|
|
let mobile = find(MOBILE_ANNO);
|
|
let queryURI = Utils.makeURI("place:folder=" + kSpecialIds.mobile);
|
|
let title = Str.sync.get("mobile.label");
|
|
|
|
// Don't add OR remove the mobile bookmarks if there's nothing.
|
|
if (PlacesUtils.bookmarks.getIdForItemAt(kSpecialIds.mobile, 0) == -1) {
|
|
if (mobile.length != 0)
|
|
PlacesUtils.bookmarks.removeItem(mobile[0]);
|
|
}
|
|
// Add the mobile bookmarks query if it doesn't exist
|
|
else if (mobile.length == 0) {
|
|
let query = PlacesUtils.bookmarks.insertBookmark(all[0], queryURI, -1, title);
|
|
PlacesUtils.annotations.setItemAnnotation(query, ORGANIZERQUERY_ANNO, MOBILE_ANNO, 0,
|
|
PlacesUtils.annotations.EXPIRE_NEVER);
|
|
PlacesUtils.annotations.setItemAnnotation(query, EXCLUDEBACKUP_ANNO, 1, 0,
|
|
PlacesUtils.annotations.EXPIRE_NEVER);
|
|
}
|
|
// Make sure the existing title is correct
|
|
else if (PlacesUtils.bookmarks.getItemTitle(mobile[0]) != title) {
|
|
PlacesUtils.bookmarks.setItemTitle(mobile[0], title);
|
|
}
|
|
|
|
this.ignoreAll = false;
|
|
},
|
|
|
|
// This method is oddly structured, but the idea is to return as quickly as
|
|
// possible -- this handler gets called *every time* a bookmark changes, for
|
|
// *each change*.
|
|
onItemChanged: function BMT_onItemChanged(itemId, property, isAnno, value,
|
|
lastModified, itemType, parentId,
|
|
guid, parentGuid) {
|
|
// Quicker checks first.
|
|
if (this.ignoreAll)
|
|
return;
|
|
|
|
if (isAnno && (ANNOS_TO_TRACK.indexOf(property) == -1))
|
|
// Ignore annotations except for the ones that we sync.
|
|
return;
|
|
|
|
// Ignore favicon changes to avoid unnecessary churn.
|
|
if (property == "favicon")
|
|
return;
|
|
|
|
if (this._ignore(itemId, parentId, guid))
|
|
return;
|
|
|
|
this._log.trace("onItemChanged: " + itemId +
|
|
(", " + property + (isAnno? " (anno)" : "")) +
|
|
(value ? (" = \"" + value + "\"") : ""));
|
|
this._add(itemId, guid);
|
|
},
|
|
|
|
onItemMoved: function BMT_onItemMoved(itemId, oldParent, oldIndex,
|
|
newParent, newIndex, itemType,
|
|
guid, oldParentGuid, newParentGuid) {
|
|
if (this._ignore(itemId, newParent, guid))
|
|
return;
|
|
|
|
this._log.trace("onItemMoved: " + itemId);
|
|
this._add(oldParent, oldParentGuid);
|
|
if (oldParent != newParent) {
|
|
this._add(itemId, guid);
|
|
this._add(newParent, newParentGuid);
|
|
}
|
|
|
|
// Remove any position annotations now that the user moved the item
|
|
PlacesUtils.annotations.removeItemAnnotation(itemId, PARENT_ANNO);
|
|
},
|
|
|
|
onBeginUpdateBatch: function () {},
|
|
onEndUpdateBatch: function () {},
|
|
onItemVisited: function () {}
|
|
};
|