mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 988070 - New PlacesUtils.promiseBookmrksTree API for retrieving bookmarks data, a generalization of PlacesBackups.getBookmarksTree. r=mak, sr=gavin.
This commit is contained in:
parent
b464257ec1
commit
ecf0f3e66c
@ -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.
|
||||
*
|
||||
|
@ -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];
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user