Bug 988070 - New PlacesUtils.promiseBookmrksTree API for retrieving bookmarks data, a generalization of PlacesBackups.getBookmarksTree. r=mak, sr=gavin.

This commit is contained in:
Asaf Romano 2014-05-22 19:06:57 +03:00
parent b464257ec1
commit ecf0f3e66c
7 changed files with 582 additions and 258 deletions

View File

@ -1049,10 +1049,6 @@ XPCOMUtils.defineLazyGetter(this, "bundle", function() {
createBundle(PLACES_STRING_BUNDLE_URI);
});
XPCOMUtils.defineLazyServiceGetter(this, "focusManager",
"@mozilla.org/focus-manager;1",
"nsIFocusManager");
/**
* This is a compatibility shim for old PUIU.ptm users.
*

View File

@ -23,8 +23,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
"resource://gre/modules/Deprecated.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
"resource://gre/modules/Sqlite.jsm");
XPCOMUtils.defineLazyGetter(this, "localFileCtor",
() => Components.Constructor("@mozilla.org/file/local;1",
@ -495,177 +493,24 @@ this.PlacesBackups = {
* * root: string describing whether this represents a root
* * children: array of child items in a folder
*/
getBookmarksTree: function () {
return Task.spawn(function* () {
let dbFilePath = OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite");
let conn = yield Sqlite.openConnection({ path: dbFilePath,
sharedMemoryCache: false });
let rows = [];
try {
rows = yield conn.execute(
"SELECT b.id, h.url, IFNULL(b.title, '') AS title, b.parent, " +
"b.position AS [index], b.type, b.dateAdded, b.lastModified, " +
"b.guid, f.url AS iconuri, " +
"( SELECT GROUP_CONCAT(t.title, ',') " +
"FROM moz_bookmarks b2 " +
"JOIN moz_bookmarks t ON t.id = +b2.parent AND t.parent = :tags_folder " +
"WHERE b2.fk = h.id " +
") AS tags, " +
"EXISTS (SELECT 1 FROM moz_items_annos WHERE item_id = b.id LIMIT 1) AS has_annos, " +
"( SELECT a.content FROM moz_annos a " +
"JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id " +
"WHERE place_id = h.id AND n.name = :charset_anno " +
") AS charset " +
"FROM moz_bookmarks b " +
"LEFT JOIN moz_bookmarks p ON p.id = b.parent " +
"LEFT JOIN moz_places h ON h.id = b.fk " +
"LEFT JOIN moz_favicons f ON f.id = h.favicon_id " +
"WHERE b.id <> :tags_folder AND b.parent <> :tags_folder AND p.parent <> :tags_folder " +
"ORDER BY b.parent, b.position",
{ tags_folder: PlacesUtils.tagsFolderId,
charset_anno: PlacesUtils.CHARSET_ANNO });
} catch(e) {
Cu.reportError("Unable to query the database " + e);
} finally {
yield conn.close();
getBookmarksTree: Task.async(function* () {
let rootGUID = yield PlacesUtils.promiseItemGUID(PlacesUtils.placesRootId);
let startTime = Date.now();
let root = yield PlacesUtils.promiseBookmarksTree(rootGUID, {
excludeItemsCallback: aItem => {
return aItem.annos &&
aItem.annos.find(a => a.name == PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
}
let startTime = Date.now();
// Create a Map for lookup and recursive building of the tree.
let itemsMap = new Map();
for (let row of rows) {
let id = row.getResultByName("id");
try {
let bookmark = sqliteRowToBookmarkObject(row);
if (itemsMap.has(id)) {
// Since children may be added before parents, we should merge with
// the existing object.
let original = itemsMap.get(id);
for (let prop of Object.getOwnPropertyNames(bookmark)) {
original[prop] = bookmark[prop];
}
bookmark = original;
}
else {
itemsMap.set(id, bookmark);
}
// Append bookmark to its parent.
if (!itemsMap.has(bookmark.parent))
itemsMap.set(bookmark.parent, {});
let parent = itemsMap.get(bookmark.parent);
if (!("children" in parent))
parent.children = [];
parent.children.push(bookmark);
} catch (e) {
Cu.reportError("Error while reading node " + id + " " + e);
}
}
// Handle excluded items, by removing entire subtrees pointed by them.
function removeFromMap(id) {
// Could have been removed by a previous call, since we can't
// predict order of items in EXCLUDE_FROM_BACKUP_ANNO.
if (itemsMap.has(id)) {
let excludedItem = itemsMap.get(id);
if (excludedItem.children) {
for (let child of excludedItem.children) {
removeFromMap(child.id);
}
}
// Remove the excluded item from its parent's children...
let parentItem = itemsMap.get(excludedItem.parent);
parentItem.children = parentItem.children.filter(aChild => aChild.id != id);
// ...then remove it from the map.
itemsMap.delete(id);
}
}
for (let id of PlacesUtils.annotations.getItemsWithAnnotation(
PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO)) {
removeFromMap(id);
}
// Report the time taken to build the tree. This doesn't take into
// account the time spent in the query since that's off the main-thread.
try {
Services.telemetry
.getHistogramById("PLACES_BACKUPS_BOOKMARKSTREE_MS")
.add(Date.now() - startTime);
} catch (ex) {
Components.utils.reportError("Unable to report telemetry.");
}
return [itemsMap.get(PlacesUtils.placesRootId), itemsMap.size];
});
}
}
/**
* Helper function to convert a Sqlite.jsm row to a bookmark object
* representation.
*
* @param aRow The Sqlite.jsm result row.
*/
function sqliteRowToBookmarkObject(aRow) {
let bookmark = {};
for (let p of [ "id" ,"guid", "title", "index", "dateAdded", "lastModified" ]) {
bookmark[p] = aRow.getResultByName(p);
}
Object.defineProperty(bookmark, "parent",
{ value: aRow.getResultByName("parent") });
let type = aRow.getResultByName("type");
// Add annotations.
if (aRow.getResultByName("has_annos")) {
try {
bookmark.annos = PlacesUtils.getAnnotationsForItem(bookmark.id);
} catch (e) {
Cu.reportError("Unexpected error while reading annotations " + e);
Services.telemetry
.getHistogramById("PLACES_BACKUPS_BOOKMARKSTREE_MS")
.add(Date.now() - startTime);
} catch (ex) {
Components.utils.reportError("Unable to report telemetry.");
}
}
switch (type) {
case Ci.nsINavBookmarksService.TYPE_BOOKMARK:
// TODO: What about shortcuts?
bookmark.type = PlacesUtils.TYPE_X_MOZ_PLACE;
// This will throw if we try to serialize an invalid url and the node will
// just be skipped.
bookmark.uri = NetUtil.newURI(aRow.getResultByName("url")).spec;
// Keywords are cached, so this should be decently fast.
let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(bookmark.id);
if (keyword)
bookmark.keyword = keyword;
let charset = aRow.getResultByName("charset");
if (charset)
bookmark.charset = charset;
let tags = aRow.getResultByName("tags");
if (tags)
bookmark.tags = tags;
let iconuri = aRow.getResultByName("iconuri");
if (iconuri)
bookmark.iconuri = iconuri;
break;
case Ci.nsINavBookmarksService.TYPE_FOLDER:
bookmark.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
// Mark root folders.
if (bookmark.id == PlacesUtils.placesRootId)
bookmark.root = "placesRoot";
else if (bookmark.id == PlacesUtils.bookmarksMenuFolderId)
bookmark.root = "bookmarksMenuFolder";
else if (bookmark.id == PlacesUtils.unfiledBookmarksFolderId)
bookmark.root = "unfiledBookmarksFolder";
else if (bookmark.id == PlacesUtils.toolbarFolderId)
bookmark.root = "toolbarFolder";
break;
case Ci.nsINavBookmarksService.TYPE_SEPARATOR:
bookmark.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
break;
default:
Cu.reportError("Unexpected bookmark type");
break;
}
return bookmark;
return [root, root.itemsCount];
})
}

View File

@ -558,6 +558,7 @@ function DefineTransaction(aRequiredProps = [], aOptionalProps = []) {
}
DefineTransaction.isStr = v => typeof(v) == "string";
DefineTransaction.isStrOrNull = v => typeof(v) == "string" || v === null;
DefineTransaction.isURI = v => v instanceof Components.interfaces.nsIURI;
DefineTransaction.isIndex = v => Number.isInteger(v) &&
v >= PlacesUtils.bookmarks.DEFAULT_INDEX;
@ -691,7 +692,9 @@ DefineTransaction.defineInputProps(["uri", "feedURI", "siteURI"],
DefineTransaction.isURI, null);
DefineTransaction.defineInputProps(["GUID", "parentGUID", "newParentGUID"],
DefineTransaction.isGUID);
DefineTransaction.defineInputProps(["title", "keyword", "postData"],
DefineTransaction.defineInputProps(["title"],
DefineTransaction.isStrOrNull, null);
DefineTransaction.defineInputProps(["keyword", "postData"],
DefineTransaction.isStr, "");
DefineTransaction.defineInputProps(["index", "newIndex"],
DefineTransaction.isIndex,

View File

@ -38,6 +38,12 @@ XPCOMUtils.defineLazyModuleGetter(this, "Services",
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
"resource://gre/modules/Sqlite.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
@ -1291,6 +1297,15 @@ this.PlacesUtils = {
}
},
/**
* Gets the shared Sqlite.jsm readonly connection to the Places database.
* This is intended to be used mostly internally, and by other Places modules.
* Outside the Places component, it should be used only as a last resort.
* Keep in mind the Places DB schema is by no means frozen or even stable.
* Your custom queries can - and will - break overtime.
*/
promiseDBConnection: () => gAsyncDBConnPromised,
/**
* Given a uri returns list of itemIds associated to it.
*
@ -1540,7 +1555,239 @@ this.PlacesUtils = {
* @resolves to the GUID.
* @rejects if there's no item for the given GUID.
*/
promiseItemId: function (aGUID) GUIDHelper.getItemId(aGUID)
promiseItemId: function (aGUID) GUIDHelper.getItemId(aGUID),
/**
* Asynchronously retrieve a JS-object representation of a places bookmarks
* item (a bookmark, a folder, or a separator) along with all of its
* descendants.
*
* @param [optional] aItemGUID
* the (topmost) item to be queried. If it's not passed, the places
* root is queried: that is, you get a representation of the entire
* bookmarks hierarchy.
* @param [optional] aOptions
* Options for customizing the query behavior, in the form of a JS
* object with any of the following properties:
* - excludeItemsCallback: a function for excluding items, along with
* their descendants. Given an item object (that has everything set
* apart its potential children data), it should return true if the
* item should be excluded. Once an item is excluded, the function
* isn't called for any of its descendants. This isn't called for
* the root item.
* WARNING: since the function may be called for each item, using
* this option can slow down the process significantly if the
* callback does anything that's not relatively trivial. It is
* highly recommended to avoid any synchronous I/O or DB queries.
*
* @return {Promise}
* @resolves to a JS object that represents either a single item or a
* bookmarks tree. Each node in the tree has the following properties set:
* - guid (string): the item's guid (same as aItemGUID for the top item).
* - [deprecated] id (number): the item's id. Only use it if you must. It'll
* be removed once the switch to guids is complete.
* - type (number): the item's type. @see PlacesUtils.TYPE_X_*
* - title (string): the item's title. If it has no title, this property
* isn't set.
* - dateAdded (number, microseconds from the epoch): the date-added value of
* the item.
* - lastModified (number, microseconds from the epoch): the last-modified
* value of the item.
* - annos (see getAnnotationsForItem): the item's annotations. This is not
* set if there are no annotations set for the item).
*
* The root object (i.e. the one for aItemGUID) also has the following
* properties set:
* - parentGUID (string): the guid of the root's parent. This isn't set if
* the root item is the places root.
* - itemsCount (number, not enumerable): the number of items, including the
* root item itself, which are represented in the resolved object.
*
* Bookmark items also have the following properties:
* - uri (string): the item's url.
* - tags (string): csv string of the bookmark's tags.
* - charset (string): the last known charset of the bookmark.
* - keyword (string): the bookmark's keyword (unset if none).
* - iconuri (string): the bookmark's favicon url.
* The last four properties are not set at all if they're irrelevant (e.g.
* |charset| is not set if no charset was previously set for the bookmark
* url).
*
* Folders may also have the following properties:
* - children (array): the folder's children information, each of them
* having the same set of properties as above.
*
* @rejects if the query failed for any reason.
* @note if aItemGUID points to a non-existent item, the returned promise is
* resolved to null.
*/
promiseBookmarksTree: Task.async(function* (aItemGUID = "", aOptions = {}) {
let createItemInfoObject = (aRow, aIncludeParentGUID) => {
let item = {};
let copyProps = (...props) => {
for (let prop of props) {
let val = aRow.getResultByName(prop);
if (val !== null)
item[prop] = val;
}
};
copyProps("id" ,"guid", "title", "index", "dateAdded", "lastModified");
if (aIncludeParentGUID)
copyProps("parentGUID");
let type = aRow.getResultByName("type");
if (type == Ci.nsINavBookmarksService.TYPE_BOOKMARK)
copyProps("charset", "tags", "iconuri");
// Add annotations.
if (aRow.getResultByName("has_annos")) {
try {
item.annos = PlacesUtils.getAnnotationsForItem(item.id);
} catch (e) {
Cu.reportError("Unexpected error while reading annotations " + e);
}
}
switch (type) {
case Ci.nsINavBookmarksService.TYPE_BOOKMARK:
item.type = PlacesUtils.TYPE_X_MOZ_PLACE;
// If this throws due to an invalid url, the item will be skipped.
item.uri = NetUtil.newURI(aRow.getResultByName("url")).spec;
// Keywords are cached, so this should be decently fast.
let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(item.id);
if (keyword)
item.keyword = keyword;
break;
case Ci.nsINavBookmarksService.TYPE_FOLDER:
item.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
// Mark root folders.
if (item.id == PlacesUtils.placesRootId)
item.root = "placesRoot";
else if (item.id == PlacesUtils.bookmarksMenuFolderId)
item.root = "bookmarksMenuFolder";
else if (item.id == PlacesUtils.unfiledBookmarksFolderId)
item.root = "unfiledBookmarksFolder";
else if (item.id == PlacesUtils.toolbarFolderId)
item.root = "toolbarFolder";
break;
case Ci.nsINavBookmarksService.TYPE_SEPARATOR:
item.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
break;
default:
Cu.reportError("Unexpected bookmark type");
break;
}
return item;
};
const QUERY_STR =
"WITH RECURSIVE " +
"descendants(fk, level, type, id, guid, parent, parentGUID, position, " +
" title, dateAdded, lastModified) AS (" +
" SELECT b1.fk, 0, b1.type, b1.id, b1.guid, b1.parent, " +
" (SELECT guid FROM moz_bookmarks WHERE id = b1.parent), " +
" b1.position, b1.title, b1.dateAdded, b1.lastModified " +
" FROM moz_bookmarks b1 WHERE b1.guid=:item_guid " +
" UNION ALL " +
" SELECT b2.fk, level + 1, b2.type, b2.id, b2.guid, b2.parent, " +
" descendants.guid, b2.position, b2.title, b2.dateAdded, " +
" b2.lastModified " +
" FROM moz_bookmarks b2 " +
" JOIN descendants ON b2.parent = descendants.id AND b2.id <> :tags_folder) " +
"SELECT d.level, d.id, d.guid, d.parent, d.parentGUID, d.type, " +
" d.position AS [index], d.title, d.dateAdded, d.lastModified, " +
" h.url, f.url AS iconuri, " +
" (SELECT GROUP_CONCAT(t.title, ',') " +
" FROM moz_bookmarks b2 " +
" JOIN moz_bookmarks t ON t.id = +b2.parent AND t.parent = :tags_folder " +
" WHERE b2.fk = h.id " +
" ) AS tags, " +
" EXISTS (SELECT 1 FROM moz_items_annos " +
" WHERE item_id = d.id LIMIT 1) AS has_annos, " +
" (SELECT a.content FROM moz_annos a " +
" JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id " +
" WHERE place_id = h.id AND n.name = :charset_anno " +
" ) AS charset " +
"FROM descendants d " +
"LEFT JOIN moz_bookmarks b3 ON b3.id = d.parent " +
"LEFT JOIN moz_places h ON h.id = d.fk " +
"LEFT JOIN moz_favicons f ON f.id = h.favicon_id " +
"ORDER BY d.level, d.parent, d.position";
if (!aItemGUID)
aItemGUID = yield this.promiseItemGUID(PlacesUtils.placesRootId);
let hasExcludeItemsCallback =
aOptions.hasOwnProperty("excludeItemsCallback");
let excludedParents = new Set();
let shouldExcludeItem = (aItem, aParentGUID) => {
let exclude = excludedParents.has(aParentGUID) ||
aOptions.excludeItemsCallback(aItem);
if (exclude) {
if (aItem.type == this.TYPE_X_MOZ_PLACE_CONTAINER)
excludedParents.add(aItem.guid);
}
return exclude;
};
let rootItem = null, rootItemCreationEx = null;
let parentsMap = new Map();
try {
let conn = yield this.promiseDBConnection();
yield conn.executeCached(QUERY_STR,
{ tags_folder: PlacesUtils.tagsFolderId,
charset_anno: PlacesUtils.CHARSET_ANNO,
item_guid: aItemGUID }, (aRow) => {
let item;
if (!rootItem) {
// This is the first row.
try {
rootItem = item = createItemInfoObject(aRow, true);
}
catch(ex) {
// If we couldn't figure out the root item, that is just as bad
// as a failed query. Bail out.
rootItemCreationEx = ex;
throw StopIteration;
}
Object.defineProperty(rootItem, "itemsCount",
{ value: 1
, writable: true
, enumerable: false
, configurable: false });
}
else {
// Our query guarantees that we always visit parents ahead of their
// children.
item = createItemInfoObject(aRow, false);
let parentGUID = aRow.getResultByName("parentGUID");
if (hasExcludeItemsCallback && shouldExcludeItem(item, parentGUID))
return;
let parentItem = parentsMap.get(parentGUID);
if ("children" in parentItem)
parentItem.children.push(item);
else
parentItem.children = [item];
rootItem.itemsCount++;
}
if (item.type == this.TYPE_X_MOZ_PLACE_CONTAINER)
parentsMap.set(item.guid, item);
});
} catch(e) {
throw new Error("Unable to query the database " + e);
}
if (rootItemCreationEx) {
throw new Error("Failed to fetch the data for the root item" +
rootItemCreationEx);
}
return rootItem;
})
};
/**
@ -1609,10 +1856,9 @@ XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "tagging",
"@mozilla.org/browser/tagging-service;1",
"nsITaggingService");
XPCOMUtils.defineLazyGetter(PlacesUtils, "livemarks", function() {
return Cc["@mozilla.org/browser/livemark-service;2"].
getService(Ci.mozIAsyncLivemarks);
});
XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "livemarks",
"@mozilla.org/browser/livemark-service;2",
"mozIAsyncLivemarks");
XPCOMUtils.defineLazyGetter(PlacesUtils, "transactionManager", function() {
let tm = Cc["@mozilla.org/transactionmanager;1"].
@ -1653,9 +1899,14 @@ XPCOMUtils.defineLazyGetter(this, "bundle", function() {
createBundle(PLACES_STRING_BUNDLE_URI);
});
XPCOMUtils.defineLazyServiceGetter(this, "focusManager",
"@mozilla.org/focus-manager;1",
"nsIFocusManager");
XPCOMUtils.defineLazyGetter(this, "gAsyncDBConnPromised",
() => Sqlite.cloneStorageConnection({
connection: PlacesUtils.history.DBConnection,
readOnly: true }).then(conn => {
PlacesUtils.registerShutdownFunction(() => conn.close());
return conn;
})
);
// Sometime soon, likely as part of the transition to mozIAsyncBookmarks,
// itemIds will be deprecated in favour of GUIDs, which play much better
@ -1671,78 +1922,46 @@ XPCOMUtils.defineLazyServiceGetter(this, "focusManager",
// working with GUIDs. So, until it does, this helper object accesses the
// Places database directly in order to switch between GUIDs and itemIds, and
// "restore" GUIDs on items re-created items.
const REASON_FINISHED = Ci.mozIStorageStatementCallback.REASON_FINISHED;
let GUIDHelper = {
// Cache for guid<->itemId paris.
GUIDsForIds: new Map(),
idsForGUIDs: new Map(),
getItemId: function (aGUID) {
if (this.idsForGUIDs.has(aGUID))
return Promise.resolve(this.idsForGUIDs.get(aGUID));
getItemId: Task.async(function* (aGUID) {
let cached = this.idsForGUIDs.get(aGUID);
if (cached !== undefined)
return cached;
let deferred = Promise.defer();
let itemId = -1;
let conn = yield PlacesUtils.promiseDBConnection();
let rows = yield conn.executeCached(
"SELECT b.id, b.guid from moz_bookmarks b WHERE b.guid = :guid LIMIT 1",
{ guid: aGUID });
if (rows.length == 0)
throw new Error("no item found for the given guid");
this._getIDStatement.params.guid = aGUID;
this._getIDStatement.executeAsync({
handleResult: function (aResultSet) {
let row = aResultSet.getNextRow();
if (row)
itemId = row.getResultByIndex(0);
},
handleCompletion: aReason => {
if (aReason == REASON_FINISHED && itemId != -1) {
this.ensureObservingRemovedItems();
this.idsForGUIDs.set(aGUID, itemId);
this.ensureObservingRemovedItems();
let itemId = rows[0].getResultByName("id");
this.idsForGUIDs.set(aGUID, itemId);
return itemId;
}),
deferred.resolve(itemId);
}
else if (itemId != -1) {
deferred.reject("no item found for the given guid");
}
else {
deferred.reject("SQLite Error: " + aReason);
}
}
});
getItemGUID: Task.async(function* (aItemId) {
let cached = this.GUIDsForIds.get(aItemId);
if (cached !== undefined)
return cached;
return deferred.promise;
},
let conn = yield PlacesUtils.promiseDBConnection();
let rows = yield conn.executeCached(
"SELECT b.id, b.guid from moz_bookmarks b WHERE b.id = :id LIMIT 1",
{ id: aItemId });
if (rows.length == 0)
throw new Error("no item found for the given itemId");
getItemGUID: function (aItemId) {
if (this.GUIDsForIds.has(aItemId))
return Promise.resolve(this.GUIDsForIds.get(aItemId));
let deferred = Promise.defer();
let guid = "";
this._getGUIDStatement.params.id = aItemId;
this._getGUIDStatement.executeAsync({
handleResult: function (aResultSet) {
let row = aResultSet.getNextRow();
if (row) {
guid = row.getResultByIndex(1);
}
},
handleCompletion: aReason => {
if (aReason == REASON_FINISHED && guid) {
this.ensureObservingRemovedItems();
this.GUIDsForIds.set(aItemId, guid);
deferred.resolve(guid);
}
else if (!guid) {
deferred.reject("no item found for the given itemId");
}
else {
deferred.reject("SQLite Error: " + aReason);
}
}
});
return deferred.promise;
},
this.ensureObservingRemovedItems();
let guid = rows[0].getResultByName("guid");
this.GUIDsForIds.set(aItemId, guid);
return guid;
}),
ensureObservingRemovedItems: function () {
if (!("observer" in this)) {
@ -1776,18 +1995,6 @@ let GUIDHelper = {
}
}
};
XPCOMUtils.defineLazyGetter(GUIDHelper, "_getIDStatement", () => {
let s = PlacesUtils.history.DBConnection.createAsyncStatement(
"SELECT b.id, b.guid from moz_bookmarks b WHERE b.guid = :guid");
PlacesUtils.registerShutdownFunction( () => s.finalize() );
return s;
});
XPCOMUtils.defineLazyGetter(GUIDHelper, "_getGUIDStatement", () => {
let s = PlacesUtils.history.DBConnection.createAsyncStatement(
"SELECT b.id, b.guid from moz_bookmarks b WHERE b.id = :id");
PlacesUtils.registerShutdownFunction( () => s.finalize() );
return s;
});
////////////////////////////////////////////////////////////////////////////////
//// Transactions handlers.
@ -1798,7 +2005,7 @@ XPCOMUtils.defineLazyGetter(GUIDHelper, "_getGUIDStatement", () => {
*/
function updateCommandsOnActiveWindow()
{
let win = focusManager.activeWindow;
let win = Services.focus.activeWindow;
if (win && win instanceof Ci.nsIDOMWindow) {
// Updating "undo" will cause a group update including "redo".
win.updateCommands("undo");

View File

@ -952,3 +952,18 @@ function promiseIsURIVisited(aURI) {
return deferred.promise;
}
/**
* Asynchronously set the favicon associated with a page.
* @param aPageURI
* The page's URI
* @param aIconURI
* The URI of the favicon to be set.
*/
function promiseSetIconForPage(aPageURI, aIconURI) {
let deferred = Promise.defer();
PlacesUtils.favicons.setAndFetchFaviconForPage(
aPageURI, aIconURI, true,
PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
() => { deferred.resolve(); });
return deferred.promise;
}

View File

@ -0,0 +1,257 @@
/* 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/. */
function* check_has_child(aParentGUID, aChildGUID) {
let parentTree = yield PlacesUtils.promiseBookmarksTree(aParentGUID);
do_check_true("children" in parentTree);
do_check_true(parentTree.children.find( e => e.guid == aChildGUID ) != null);
}
function* compareToNode(aItem, aNode, aIsRootItem, aExcludedGUIDs = []) {
// itemId==-1 indicates a non-bookmark node, which is unexpected.
do_check_neq(aNode.itemId, -1);
function check_unset(...aProps) {
aProps.forEach( p => { do_check_false(p in aItem); } );
}
function strict_eq_check(v1, v2) {
dump("v1: " + v1 + " v2: " + v2 + "\n");
do_check_eq(typeof v1, typeof v2);
do_check_eq(v1, v2);
}
function compare_prop(aItemProp, aNodeProp = aItemProp, aOptional = false) {
if (aOptional && aNode[aNodeProp] === null)
check_unset(aItemProp);
else
strict_eq_check(aItem[aItemProp], aNode[aNodeProp]);
}
function compare_prop_to_value(aItemProp, aValue, aOptional = true) {
if (aOptional && aValue === null)
check_unset(aItemProp);
else
strict_eq_check(aItem[aItemProp], aValue);
}
// Bug 1013053 - bookmarkIndex is unavailable for the query's root
if (aNode.bookmarkIndex == -1) {
compare_prop_to_value("index",
PlacesUtils.bookmarks.getItemIndex(aNode.itemId),
false);
}
else {
compare_prop("index", "bookmarkIndex");
}
compare_prop("dateAdded");
compare_prop("lastModified");
if (aIsRootItem && aNode.itemId != PlacesUtils.placesRootId) {
do_check_true("parentGUID" in aItem);
yield check_has_child(aItem.parentGUID, aItem.guid)
}
else {
check_unset("parentGUID");
}
let expectedAnnos = PlacesUtils.getAnnotationsForItem(aItem.id);
if (expectedAnnos.length > 0) {
let annosToString = annos => {
return [(a.name + ":" + a.value) for (a of annos)].sort().join(",");
};
do_check_true(Array.isArray(aItem.annos))
do_check_eq(annosToString(aItem.annos), annosToString(expectedAnnos));
}
else {
check_unset("annos");
}
const BOOKMARK_ONLY_PROPS = ["uri", "iconuri", "tags", "charset", "keyword"];
const FOLDER_ONLY_PROPS = ["children", "root"];
let nodesCount = 1;
switch (aNode.type) {
case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER:
do_check_eq(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER);
compare_prop("title", "title", true);
check_unset(...BOOKMARK_ONLY_PROPS);
let expectedChildrenNodes = [];
PlacesUtils.asContainer(aNode);
if (!aNode.containerOpen)
aNode.containerOpen = true;
for (let i = 0; i < aNode.childCount; i++) {
let childNode = aNode.getChild(i);
if (childNode.itemId == PlacesUtils.tagsFolderId ||
aExcludedGUIDs.indexOf(childNode.bookmarkGuid) != -1) {
continue;
}
expectedChildrenNodes.push(childNode);
}
if (expectedChildrenNodes.length > 0) {
do_check_true(Array.isArray(aItem.children));
do_check_eq(aItem.children.length, expectedChildrenNodes.length);
for (let i = 0; i < aItem.children.length; i++) {
nodesCount +=
yield compareToNode(aItem.children[i], expectedChildrenNodes[i],
false, aExcludedGUIDs);
}
}
else {
check_unset("children");
}
switch (aItem.id) {
case PlacesUtils.placesRootId:
compare_prop_to_value("root", "placesRoot");
break;
case PlacesUtils.bookmarksMenuFolderId:
compare_prop_to_value("root", "bookmarksMenuFolder");
break;
case PlacesUtils.toolbarFolderId:
compare_prop_to_value("root", "toolbarFolder");
break;
case PlacesUtils.unfiledBookmarksFolderId:
compare_prop_to_value("root", "unfiledBookmarksFolder");
break;
default:
check_unset("root");
}
break;
case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR:
do_check_eq(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR);
check_unset(...BOOKMARK_ONLY_PROPS, ...FOLDER_ONLY_PROPS);
break;
default:
do_check_eq(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE);
compare_prop("uri");
// node.tags's format is "a, b" whilst promiseBoookmarksTree is "a,b"
if (aNode.tags === null)
check_unset("tags");
else
compare_prop_to_value("tags", aNode.tags.replace(/, /g, ","), false);
if (aNode.icon) {
let nodeIconData = aNode.icon.replace("moz-anno:favicon:","");
compare_prop_to_value("iconuri", nodeIconData);
}
else {
check_unset(aItem.iconuri);
}
check_unset(...FOLDER_ONLY_PROPS);
let itemURI = uri(aNode.uri);
compare_prop_to_value("charset",
yield PlacesUtils.getCharsetForURI(itemURI));
compare_prop_to_value("keyword",
PlacesUtils.bookmarks
.getKeywordForBookmark(aNode.itemId));
if ("title" in aItem)
compare_prop("title");
else
do_check_null(aNode.title);
}
if (aIsRootItem)
do_check_eq(aItem.itemsCount, nodesCount);
return nodesCount;
}
let itemsCount = 0;
function* new_bookmark(aInfo) {
let currentItem = ++itemsCount;
if (!("uri" in aInfo))
aInfo.uri = uri("http://test.item." + itemsCount);
if (!("title" in aInfo))
aInfo.title = "Test Item (bookmark) " + itemsCount;
yield PlacesTransactions.transact(PlacesTransactions.NewBookmark(aInfo));
}
function* new_folder(aInfo) {
if (!("title" in aInfo))
aInfo.title = "Test Item (folder) " + itemsCount;
return yield PlacesTransactions.transact(PlacesTransactions.NewFolder(aInfo));
}
// Walks a result nodes tree and test promiseBookmarksTree for each node.
// DO NOT COPY THIS LOGIC: It is done here to accomplish a more comprehensive
// test of the API (the entire hierarchy data is available in the very test).
function* test_promiseBookmarksTreeForEachNode(aNode, aOptions, aExcludedGUIDs) {
do_check_true(aNode.bookmarkGuid && aNode.bookmarkGuid.length > 0);
let item = yield PlacesUtils.promiseBookmarksTree(aNode.bookmarkGuid, aOptions);
yield* compareToNode(item, aNode, true, aExcludedGUIDs);
for (let i = 0; i < aNode.childCount; i++) {
let child = aNode.getChild(i);
if (child.itemId != PlacesUtils.tagsFolderId)
yield test_promiseBookmarksTreeForEachNode(child, {}, aExcludedGUIDs);
}
return item;
}
function* test_promiseBookmarksTreeAgainstResult(aItemGUID = "", aOptions, aExcludedGUIDs) {
let itemId = aItemGUID ?
yield PlacesUtils.promiseItemId(aItemGUID) : PlacesUtils.placesRootId;
let node = PlacesUtils.getFolderContents(itemId).root;
return yield test_promiseBookmarksTreeForEachNode(node, aOptions, aExcludedGUIDs);
}
add_task(function* () {
// Add some bookmarks to cover various use cases.
let toolbarGUID =
yield PlacesUtils.promiseItemGUID(PlacesUtils.toolbarFolderId);
let menuGUID =
yield PlacesUtils.promiseItemGUID(PlacesUtils.bookmarksMenuFolderId);
yield new_bookmark({ parentGUID: toolbarGUID });
yield new_folder({ parentGUID: menuGUID
, annotations: [{ name: "TestAnnoA", value: "TestVal"
, name: "TestAnnoB", value: 0 }]});
yield PlacesTransactions.transact(
PlacesTransactions.NewSeparator({ parentGUID: menuGUID }));
let folderGUID = yield new_folder({ parentGUID: menuGUID });
yield new_bookmark({ title: null
, parentGUID: folderGUID
, keyword: "test_keyword"
, tags: ["TestTagA", "TestTagB"]
, annotations: [{ name: "TestAnnoA", value: "TestVal2"}]});
let urlWithCharsetAndFavicon = uri("http://charset.and.favicon");
yield new_bookmark({ parentGUID: folderGUID, uri: urlWithCharsetAndFavicon });
yield PlacesUtils.setCharsetForURI(urlWithCharsetAndFavicon, "UTF-8");
yield promiseSetIconForPage(urlWithCharsetAndFavicon, SMALLPNG_DATA_URI);
// Test the default places root without specifying it.
yield test_promiseBookmarksTreeAgainstResult();
// Do specify it
let rootGUID = yield PlacesUtils.promiseItemGUID(PlacesUtils.placesRootId);
yield test_promiseBookmarksTreeAgainstResult(rootGUID);
// Exclude the bookmarks menu.
// The calllback should be four times - once for the toolbar, once for
// the bookmark we inserted under, and once for the menu (and not
// at all for any of its descendants) and once for the unsorted bookmarks
// folder. However, promiseBookmarksTree is called multiple times, so
// rather than counting the calls, we count the number of unique items
// passed in.
let guidsPassedToExcludeCallback = new Set();
let placesRootWithoutTheMenu =
yield test_promiseBookmarksTreeAgainstResult(rootGUID, {
excludeItemsCallback: aItem => {
guidsPassedToExcludeCallback.add(aItem.guid);
return aItem.root == "bookmarksMenuFolder";
}
}, [menuGUID]);
do_check_eq(guidsPassedToExcludeCallback.size, 4);
do_check_eq(placesRootWithoutTheMenu.children.length, 2);
});
function run_test() {
run_next_test();
}

View File

@ -126,6 +126,7 @@ skip-if = os == "android"
skip-if = os == "android"
[test_preventive_maintenance_runTasks.js]
[test_priorityUrlProvider.js]
[test_promiseBookmarksTree.js]
[test_removeVisitsByTimeframe.js]
# Bug 676989: test hangs consistently on Android
skip-if = os == "android"