Bug 1302288 - Implement PlacesSyncUtils.bookmarks.fetch. r=markh

MozReview-Commit-ID: 2CcE8DxHott

--HG--
extra : rebase_source : 7af66f06b889ca956cde0ff4499fcc6e74336e5c
This commit is contained in:
Kit Cambridge 2016-09-19 14:53:43 -07:00
parent 25e23d96ea
commit 07d9b90407
3 changed files with 421 additions and 125 deletions

View File

@ -52,6 +52,26 @@ const MOBILE_ANNO = "MobileBookmarks";
// the tracker doesn't currently distinguish between the two.
const IGNORED_SOURCES = [SOURCE_SYNC, SOURCE_IMPORT, SOURCE_IMPORT_REPLACE];
// Returns the constructor for a bookmark record type.
function 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;
}
return null;
}
this.PlacesItem = function PlacesItem(collection, id, type) {
CryptoWrapper.call(this, collection, id);
this.type = type || "item";
@ -69,22 +89,11 @@ PlacesItem.prototype = {
},
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;
let recordObj = getTypeObject(type);
if (!recordObj) {
throw new Error("Unknown places item object type: " + type);
}
throw "Unknown places item object type: " + type;
return recordObj;
},
__proto__: CryptoWrapper.prototype,
@ -99,6 +108,13 @@ PlacesItem.prototype = {
parentSyncId: this.parentid,
};
},
// Populates the record from a Sync bookmark object returned from
// `PlacesSyncUtils.bookmarks.fetch`.
fromSyncBookmark(item) {
this.parentid = item.parentSyncId;
this.parentName = item.parentTitle;
},
};
Utils.deferGetSet(PlacesItem,
@ -122,6 +138,16 @@ Bookmark.prototype = {
info.keyword = this.keyword;
return info;
},
fromSyncBookmark(item) {
PlacesItem.prototype.fromSyncBookmark.call(this, item);
this.title = item.title;
this.bmkUri = item.url.href;
this.description = item.description;
this.loadInSidebar = item.loadInSidebar;
this.tags = item.tags;
this.keyword = item.keyword;
},
};
Utils.deferGetSet(Bookmark,
@ -142,6 +168,12 @@ BookmarkQuery.prototype = {
info.query = this.queryId;
return info;
},
fromSyncBookmark(item) {
Bookmark.prototype.fromSyncBookmark.call(this, item);
this.folderName = item.folder;
this.queryId = item.query;
},
};
Utils.deferGetSet(BookmarkQuery,
@ -161,6 +193,13 @@ BookmarkFolder.prototype = {
info.title = this.title;
return info;
},
fromSyncBookmark(item) {
PlacesItem.prototype.fromSyncBookmark.call(this, item);
this.title = item.title;
this.description = item.description;
this.children = item.childSyncIds;
},
};
Utils.deferGetSet(BookmarkFolder, "cleartext", ["description", "title",
@ -179,6 +218,14 @@ Livemark.prototype = {
info.site = this.siteUri;
return info;
},
fromSyncBookmark(item) {
BookmarkFolder.prototype.fromSyncBookmark.call(this, item);
this.feedUri = item.feed.href;
if (item.site) {
this.siteUri = item.site.href;
}
},
};
Utils.deferGetSet(Livemark, "cleartext", ["siteUri", "feedUri"]);
@ -189,6 +236,11 @@ this.BookmarkSeparator = function BookmarkSeparator(collection, id) {
BookmarkSeparator.prototype = {
__proto__: PlacesItem.prototype,
_logName: "Sync.Record.Separator",
fromSyncBookmark(item) {
PlacesItem.prototype.fromSyncBookmark.call(this, item);
this.pos = item.index;
},
};
Utils.deferGetSet(BookmarkSeparator, "cleartext", "pos");
@ -716,121 +768,23 @@ BookmarksStore.prototype = {
Async.promiseSpinningly(PlacesSyncUtils.bookmarks.changeGuid(oldID, newID));
},
_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,
PlacesSyncUtils.bookmarks.DESCRIPTION_ANNO);
} catch (e) {
return null;
}
},
_isLoadInSidebar: function BStore__isLoadInSidebar(id) {
return PlacesUtils.annotations.itemHasAnnotation(id,
PlacesSyncUtils.bookmarks.SIDEBAR_ANNO);
},
// 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);
let item = Async.promiseSpinningly(PlacesSyncUtils.bookmarks.fetch(id));
if (!item) { // deleted item
let 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,
PlacesSyncUtils.bookmarks.SMART_BOOKMARKS_ANNO);
if (anno != null) {
this._log.trace("query anno: " +
PlacesSyncUtils.bookmarks.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 = Async.promiseSpinningly(
PlacesSyncUtils.bookmarks.fetchChildSyncIds(id));
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));
let recordObj = getTypeObject(item.kind);
if (!recordObj) {
this._log.warn("Unknown item type, cannot serialize: " + item.kind);
recordObj = PlacesItem;
}
let record = new recordObj(collection, id);
record.fromSyncBookmark(item);
record.parentid = this.GUIDForId(parent);
record.sortindex = this._calculateIndex(record);
return record;

View File

@ -264,6 +264,83 @@ const BookmarkSyncUtils = PlacesSyncUtils.bookmarks = Object.freeze({
let insertInfo = validateNewBookmark(info);
return insertSyncBookmark(insertInfo);
}),
/**
* Fetches a Sync bookmark object for an item in the tree. The object contains
* the following properties, depending on the item's kind:
*
* - kind (all): A string representing the item's kind.
* - syncId (all): The item's sync ID.
* - parentSyncId (all): The sync ID of the item's parent.
* - parentTitle (all): The title of the item's parent, used for de-duping.
* Omitted for the Places root and parents with empty titles.
* - title ("bookmark", "folder", "livemark", "query"): The item's title.
* Omitted if empty.
* - url ("bookmark", "query"): The item's URL.
* - tags ("bookmark", "query"): An array containing the item's tags.
* - keyword ("bookmark"): The bookmark's keyword, if one exists.
* - description ("bookmark", "folder", "livemark"): The item's description.
* Omitted if one isn't set.
* - loadInSidebar ("bookmark", "query"): Whether to load the bookmark in
* the sidebar. Always `false` for queries.
* - feed ("livemark"): A `URL` object pointing to the livemark's feed URL.
* - site ("livemark"): A `URL` object pointing to the livemark's site URL,
* or `null` if one isn't set.
* - childSyncIds ("folder"): An array containing the sync IDs of the item's
* children, used to determine child order.
* - folder ("query"): The tag folder name, if this is a tag query.
* - query ("query"): The smart bookmark query name, if this is a smart
* bookmark.
* - index ("separator"): The separator's position within its parent.
*/
fetch: Task.async(function* (syncId) {
let guid = BookmarkSyncUtils.syncIdToGuid(syncId);
let bookmarkItem = yield PlacesUtils.bookmarks.fetch(guid);
if (!bookmarkItem) {
return null;
}
// Convert the Places bookmark object to a Sync bookmark and add
// kind-specific properties.
let kind = yield getKindForItem(bookmarkItem);
let item;
switch (kind) {
case BookmarkSyncUtils.KINDS.BOOKMARK:
case BookmarkSyncUtils.KINDS.MICROSUMMARY:
item = yield fetchBookmarkItem(bookmarkItem);
break;
case BookmarkSyncUtils.KINDS.QUERY:
item = yield fetchQueryItem(bookmarkItem);
break;
case BookmarkSyncUtils.KINDS.FOLDER:
item = yield fetchFolderItem(bookmarkItem);
break;
case BookmarkSyncUtils.KINDS.LIVEMARK:
item = yield fetchLivemarkItem(bookmarkItem);
break;
case BookmarkSyncUtils.KINDS.SEPARATOR:
item = yield placesBookmarkToSyncBookmark(bookmarkItem);
item.index = bookmarkItem.index;
break;
default:
throw new Error(`Unknown bookmark kind: ${kind}`);
}
// Sync uses the parent title for de-duping.
if (bookmarkItem.parentGuid) {
let parent = yield PlacesUtils.bookmarks.fetch(bookmarkItem.parentGuid);
if ("title" in parent) {
item.parentTitle = parent.title;
}
}
return item;
}),
});
XPCOMUtils.defineLazyGetter(this, "BookmarkSyncLog", () => {
@ -972,3 +1049,129 @@ function syncBookmarkToPlacesBookmark(info) {
return bookmarkInfo;
}
// Creates and returns a Sync bookmark object containing the bookmark's
// tags, keyword, description, and whether it loads in the sidebar.
var fetchBookmarkItem = Task.async(function* (bookmarkItem) {
let itemId = yield PlacesUtils.promiseItemId(bookmarkItem.guid);
let item = yield placesBookmarkToSyncBookmark(bookmarkItem);
item.tags = PlacesUtils.tagging.getTagsForURI(
PlacesUtils.toURI(bookmarkItem.url), {});
let keywordEntry = yield PlacesUtils.keywords.fetch({
url: bookmarkItem.url,
});
if (keywordEntry) {
item.keyword = keywordEntry.keyword;
}
let description = getItemDescription(itemId);
if (description) {
item.description = description;
}
item.loadInSidebar = PlacesUtils.annotations.itemHasAnnotation(itemId,
BookmarkSyncUtils.SIDEBAR_ANNO);
return item;
});
// Creates and returns a Sync bookmark object containing the folder's
// description and children.
var fetchFolderItem = Task.async(function* (bookmarkItem) {
let itemId = yield PlacesUtils.promiseItemId(bookmarkItem.guid);
let item = yield placesBookmarkToSyncBookmark(bookmarkItem);
let description = getItemDescription(itemId);
if (description) {
item.description = description;
}
let db = yield PlacesUtils.promiseDBConnection();
let children = yield fetchAllChildren(db, bookmarkItem.guid);
item.childSyncIds = children.map(child =>
BookmarkSyncUtils.guidToSyncId(child.guid)
);
return item;
});
// Creates and returns a Sync bookmark object containing the livemark's
// description, children (none), feed URI, and site URI.
var fetchLivemarkItem = Task.async(function* (bookmarkItem) {
let itemId = yield PlacesUtils.promiseItemId(bookmarkItem.guid);
let item = yield placesBookmarkToSyncBookmark(bookmarkItem);
let description = getItemDescription(itemId);
if (description) {
item.description = description;
}
let feedAnno = PlacesUtils.annotations.getItemAnnotation(itemId,
PlacesUtils.LMANNO_FEEDURI);
item.feed = new URL(feedAnno);
let siteAnno = null;
try {
siteAnno = PlacesUtils.annotations.getItemAnnotation(itemId,
PlacesUtils.LMANNO_SITEURI);
} catch (ex) {}
if (siteAnno != null) {
item.site = new URL(siteAnno);
}
return item;
});
// Creates and returns a Sync bookmark object containing the query's tag
// folder name and smart bookmark query ID.
var fetchQueryItem = Task.async(function* (bookmarkItem) {
let itemId = yield PlacesUtils.promiseItemId(bookmarkItem.guid);
let item = yield placesBookmarkToSyncBookmark(bookmarkItem);
let description = getItemDescription(itemId);
if (description) {
item.description = description;
}
let folder = null;
let params = new URLSearchParams(bookmarkItem.url.pathname);
let tagFolderId = +params.get("folder");
if (tagFolderId) {
try {
let tagFolderGuid = yield PlacesUtils.promiseItemGuid(tagFolderId);
let tagFolder = yield PlacesUtils.bookmarks.fetch(tagFolderGuid);
folder = tagFolder.title;
} catch (ex) {
BookmarkSyncLog.warn("fetchQueryItem: Query " + bookmarkItem.url.href +
" points to nonexistent folder " + tagFolderId, ex);
}
}
if (folder != null) {
item.folder = folder;
}
let query = null;
try {
// Throws if the bookmark doesn't have the smart bookmark anno.
query = PlacesUtils.annotations.getItemAnnotation(itemId,
BookmarkSyncUtils.SMART_BOOKMARKS_ANNO);
} catch (ex) {}
if (query != null) {
item.query = query;
}
return item;
});
// Returns an item's description, or `null` if one isn't set.
function getItemDescription(id) {
try {
return PlacesUtils.annotations.getItemAnnotation(id,
BookmarkSyncUtils.DESCRIPTION_ANNO);
} catch (ex) {}
return null;
}

View File

@ -343,13 +343,10 @@ add_task(function* test_update_annos() {
let guids = yield populateTree(PlacesUtils.bookmarks.menuGuid, {
kind: "folder",
title: "folder",
description: "Folder description",
}, {
kind: "bookmark",
title: "bmk",
url: "https://example.com",
description: "Bookmark description",
loadInSidebar: true,
});
do_print("Add folder description");
@ -1004,3 +1001,145 @@ add_task(function* test_insert_orphans() {
yield PlacesUtils.bookmarks.eraseEverything();
});
add_task(function* test_fetch() {
let folder = yield PlacesSyncUtils.bookmarks.insert({
syncId: makeGuid(),
parentSyncId: "menu",
kind: "folder",
description: "Folder description",
});
let bmk = yield PlacesSyncUtils.bookmarks.insert({
syncId: makeGuid(),
parentSyncId: "menu",
kind: "bookmark",
url: "https://example.com",
description: "Bookmark description",
loadInSidebar: true,
tags: ["taggy"],
});
let folderBmk = yield PlacesSyncUtils.bookmarks.insert({
syncId: makeGuid(),
parentSyncId: folder.syncId,
kind: "bookmark",
url: "https://example.org",
keyword: "kw",
});
let folderSep = yield PlacesSyncUtils.bookmarks.insert({
syncId: makeGuid(),
parentSyncId: folder.syncId,
kind: "separator",
});
let tagQuery = yield PlacesSyncUtils.bookmarks.insert({
kind: "query",
syncId: makeGuid(),
parentSyncId: "toolbar",
url: "place:type=7&folder=90",
folder: "taggy",
title: "Tagged stuff",
});
let [, tagFolderId] = /\bfolder=(\d+)\b/.exec(tagQuery.url.pathname);
let smartBmk = yield PlacesSyncUtils.bookmarks.insert({
kind: "query",
syncId: makeGuid(),
parentSyncId: "toolbar",
url: "place:folder=TOOLBAR",
query: "BookmarksToolbar",
title: "Bookmarks toolbar query",
});
do_print("Fetch empty folder with description");
{
let item = yield PlacesSyncUtils.bookmarks.fetch(folder.syncId);
deepEqual(item, {
syncId: folder.syncId,
kind: "folder",
parentSyncId: "menu",
description: "Folder description",
childSyncIds: [folderBmk.syncId, folderSep.syncId],
parentTitle: "Bookmarks Menu",
}, "Should include description, children, and parent title in folder");
}
do_print("Fetch bookmark with description, sidebar anno, and tags");
{
let item = yield PlacesSyncUtils.bookmarks.fetch(bmk.syncId);
deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId", "url",
"tags", "description", "loadInSidebar", "parentTitle"].sort(),
"Should include bookmark-specific properties");
equal(item.syncId, bmk.syncId, "Sync ID should match");
equal(item.url.href, "https://example.com/", "Should return URL");
equal(item.parentSyncId, "menu", "Should return parent sync ID");
deepEqual(item.tags, ["taggy"], "Should return tags");
equal(item.description, "Bookmark description", "Should return bookmark description");
strictEqual(item.loadInSidebar, true, "Should return sidebar anno");
equal(item.parentTitle, "Bookmarks Menu", "Should return parent title");
}
do_print("Fetch bookmark with keyword; without parent title or annos");
{
let item = yield PlacesSyncUtils.bookmarks.fetch(folderBmk.syncId);
deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId",
"url", "keyword", "tags", "loadInSidebar"].sort(),
"Should omit blank bookmark-specific properties");
strictEqual(item.loadInSidebar, false, "Should not load bookmark in sidebar");
deepEqual(item.tags, [], "Tags should be empty");
equal(item.keyword, "kw", "Should return keyword");
}
do_print("Fetch separator");
{
let item = yield PlacesSyncUtils.bookmarks.fetch(folderSep.syncId);
strictEqual(item.index, 1, "Should return separator position");
}
do_print("Fetch tag query");
{
let item = yield PlacesSyncUtils.bookmarks.fetch(tagQuery.syncId);
deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId",
"url", "title", "folder", "parentTitle"].sort(),
"Should include query-specific properties");
equal(item.url.href, `place:type=7&folder=${tagFolderId}`, "Should not rewrite outgoing tag queries");
equal(item.folder, "taggy", "Should return tag name for tag queries");
}
do_print("Fetch smart bookmark");
{
let item = yield PlacesSyncUtils.bookmarks.fetch(smartBmk.syncId);
deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId",
"url", "title", "query", "parentTitle"].sort(),
"Should include smart bookmark-specific properties");
equal(item.query, "BookmarksToolbar", "Should return query name for smart bookmarks");
}
yield PlacesUtils.bookmarks.eraseEverything();
});
add_task(function* test_fetch_livemark() {
let { server, site, stopServer } = makeLivemarkServer();
try {
do_print("Create livemark");
let livemark = yield PlacesUtils.livemarks.addLivemark({
parentGuid: PlacesUtils.bookmarks.menuGuid,
feedURI: uri(site + "/feed/1"),
siteURI: uri(site),
index: PlacesUtils.bookmarks.DEFAULT_INDEX,
});
PlacesUtils.annotations.setItemAnnotation(livemark.id, DESCRIPTION_ANNO,
"Livemark description", 0, PlacesUtils.annotations.EXPIRE_NEVER);
do_print("Fetch livemark");
let item = yield PlacesSyncUtils.bookmarks.fetch(livemark.guid);
deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId",
"description", "feed", "site", "parentTitle"].sort(),
"Should include livemark-specific properties");
equal(item.description, "Livemark description", "Should return description");
equal(item.feed.href, site + "/feed/1", "Should return feed URL");
equal(item.site.href, site + "/", "Should return site URL");
} finally {
yield stopServer();
}
yield PlacesUtils.bookmarks.eraseEverything();
});