gecko/toolkit/components/places/BookmarkJSONUtils.jsm

797 lines
29 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 = [ "BookmarkJSONUtils" ];
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
this.BookmarkJSONUtils = Object.freeze({
/**
* Import bookmarks from a url.
*
* @param aURL
* url of the bookmark data.
* @param aReplace
* Boolean if true, replace existing bookmarks, else merge.
*
* @return {Promise}
* @resolves When the new bookmarks have been created.
* @rejects JavaScript exception.
*/
importFromURL: function BJU_importFromURL(aURL, aReplace) {
let importer = new BookmarkImporter();
return importer.importFromURL(aURL, aReplace);
},
/**
* Restores bookmarks and tags from a JSON file.
* @note any item annotated with "places/excludeFromBackup" won't be removed
* before executing the restore.
*
* @param aFile
* nsIFile of bookmarks in JSON format to be restored.
* @param aReplace
* Boolean if true, replace existing bookmarks, else merge.
*
* @return {Promise}
* @resolves When the new bookmarks have been created.
* @rejects JavaScript exception.
*/
importFromFile: function BJU_importFromFile(aFile, aReplace) {
let importer = new BookmarkImporter();
return importer.importFromFile(aFile, aReplace);
},
/**
* Serializes bookmarks using JSON, and writes to the supplied file.
*
* @param aLocalFile
* nsIFile for the "bookmarks.json" file to be created.
*
* @return {Promise}
* @resolves When the file has been created.
* @rejects JavaScript exception.
*/
exportToFile: function BJU_exportToFile(aLocalFile) {
let exporter = new BookmarkExporter();
return exporter.exportToFile(aLocalFile);
},
/**
* Takes a JSON-serialized node and inserts it into the db.
*
* @param aData
* The unwrapped data blob of dropped or pasted data.
* @param aContainer
* The container the data was dropped or pasted into
* @param aIndex
* The index within the container the item was dropped or pasted at
* @return {Promise}
* @resolves an array containing of maps of old folder ids to new folder ids,
* and an array of saved search ids that need to be fixed up.
* eg: [[[oldFolder1, newFolder1]], [search1]]
* @rejects JavaScript exception.
*/
importJSONNode: function BJU_importJSONNode(aData, aContainer, aIndex,
aGrandParentId) {
let importer = new BookmarkImporter();
return importer.importJSONNode(aData, aContainer, aIndex, aGrandParentId);
},
/**
* Serializes the given node (and all its descendents) as JSON
* and writes the serialization to the given output stream.
*
* @param aNode
* An nsINavHistoryResultNode
* @param aStream
* An nsIOutputStream. NOTE: it only uses the write(str, len)
* method of nsIOutputStream. The caller is responsible for
* closing the stream.
* @param aIsUICommand
* Boolean - If true, modifies serialization so that each node self-contained.
* For Example, tags are serialized inline with each bookmark.
* @param aResolveShortcuts
* Converts folder shortcuts into actual folders.
* @param aExcludeItems
* An array of item ids that should not be written to the backup.
* @return {Promise}
* @resolves When node have been serialized and wrote to output stream.
* @rejects JavaScript exception.
*/
serializeNodeAsJSONToOutputStream: function BJU_serializeNodeAsJSONToOutputStream(
aNode, aStream, aIsUICommand, aResolveShortcuts, aExcludeItems) {
let deferred = Promise.defer();
Services.tm.mainThread.dispatch(function() {
try {
BookmarkNode.serializeAsJSONToOutputStream(
aNode, aStream, aIsUICommand, aResolveShortcuts, aExcludeItems);
deferred.resolve();
} catch (ex) {
deferred.reject(ex);
}
}, Ci.nsIThread.DISPATCH_NORMAL);
return deferred.promise;
}
});
function BookmarkImporter() {}
BookmarkImporter.prototype = {
/**
* Import bookmarks from a file.
*
* @param aFile
* the bookmark file.
* @param aReplace
* Boolean if true, replace existing bookmarks, else merge.
*
* @return {Promise}
* @resolves When the new bookmarks have been created.
* @rejects JavaScript exception.
*/
importFromFile: function(aFile, aReplace) {
if (aFile.exists()) {
return this.importFromURL(NetUtil.newURI(aFile).spec, aReplace);
}
notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN);
return Task.spawn(function() {
notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED);
throw new Error("File does not exist.");
});
},
/**
* Import bookmarks from a url.
*
* @param aURL
* url of the bookmark data.
* @param aReplace
* Boolean if true, replace existing bookmarks, else merge.
*
* @return {Promise}
* @resolves When the new bookmarks have been created.
* @rejects JavaScript exception.
*/
importFromURL: function BI_importFromURL(aURL, aReplace) {
let deferred = Promise.defer();
notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN);
let streamObserver = {
onStreamComplete: function (aLoader, aContext, aStatus, aLength,
aResult) {
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
Task.spawn(function() {
try {
let jsonString =
converter.convertFromByteArray(aResult, aResult.length);
yield this.importFromJSON(jsonString, aReplace);
notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS);
deferred.resolve();
} catch (ex) {
notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED);
Cu.reportError("Failed to import from URL: " + ex);
deferred.reject(ex);
}
}.bind(this));
}.bind(this)
};
try {
let channel = Services.io.newChannelFromURI(NetUtil.newURI(aURL));
let streamLoader = Cc["@mozilla.org/network/stream-loader;1"].
createInstance(Ci.nsIStreamLoader);
streamLoader.init(streamObserver);
channel.asyncOpen(streamLoader, channel);
} catch (ex) {
notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED);
Cu.reportError("Failed to import from URL: " + ex);
deferred.reject(ex);
}
return deferred.promise;
},
/**
* Import bookmarks from a JSON string.
*
* @param aString
* JSON string of serialized bookmark data.
* @param aReplace
* Boolean if true, replace existing bookmarks, else merge.
*/
importFromJSON: function BI_importFromJSON(aString, aReplace) {
let deferred = Promise.defer();
let nodes =
PlacesUtils.unwrapNodes(aString, PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER);
if (nodes.length == 0 || !nodes[0].children ||
nodes[0].children.length == 0) {
Services.tm.mainThread.dispatch(function() {
deferred.resolve(); // Nothing to restore
}, Ci.nsIThread.DISPATCH_NORMAL);
} else {
// Ensure tag folder gets processed last
nodes[0].children.sort(function sortRoots(aNode, bNode) {
return (aNode.root && aNode.root == "tagsFolder") ? 1 :
(bNode.root && bNode.root == "tagsFolder") ? -1 : 0;
});
let batch = {
nodes: nodes[0].children,
runBatched: function runBatched() {
if (aReplace) {
// Get roots excluded from the backup, we will not remove them
// before restoring.
let excludeItems = PlacesUtils.annotations.getItemsWithAnnotation(
PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
// Delete existing children of the root node, excepting:
// 1. special folders: delete the child nodes
// 2. tags folder: untag via the tagging api
let root = PlacesUtils.getFolderContents(PlacesUtils.placesRootId,
false, false).root;
let childIds = [];
for (let i = 0; i < root.childCount; i++) {
let childId = root.getChild(i).itemId;
if (excludeItems.indexOf(childId) == -1 &&
childId != PlacesUtils.tagsFolderId) {
childIds.push(childId);
}
}
root.containerOpen = false;
for (let i = 0; i < childIds.length; i++) {
let rootItemId = childIds[i];
if (PlacesUtils.isRootItem(rootItemId)) {
PlacesUtils.bookmarks.removeFolderChildren(rootItemId);
} else {
PlacesUtils.bookmarks.removeItem(rootItemId);
}
}
}
let searchIds = [];
let folderIdMap = [];
Task.spawn(function() {
for (let node of batch.nodes) {
if (!node.children || node.children.length == 0)
continue; // Nothing to restore for this root
if (node.root) {
let container = PlacesUtils.placesRootId; // Default to places root
switch (node.root) {
case "bookmarksMenuFolder":
container = PlacesUtils.bookmarksMenuFolderId;
break;
case "tagsFolder":
container = PlacesUtils.tagsFolderId;
break;
case "unfiledBookmarksFolder":
container = PlacesUtils.unfiledBookmarksFolderId;
break;
case "toolbarFolder":
container = PlacesUtils.toolbarFolderId;
break;
}
// Insert the data into the db
for (let child of node.children) {
let index = child.index;
let [folders, searches] =
yield this.importJSONNode(child, container, index, 0);
for (let i = 0; i < folders.length; i++) {
if (folders[i])
folderIdMap[i] = folders[i];
}
searchIds = searchIds.concat(searches);
}
} else {
yield this.importJSONNode(
node, PlacesUtils.placesRootId, node.index, 0);
}
}
// Fixup imported place: uris that contain folders
searchIds.forEach(function(aId) {
let oldURI = PlacesUtils.bookmarks.getBookmarkURI(aId);
let uri = fixupQuery(oldURI, folderIdMap);
if (!uri.equals(oldURI)) {
PlacesUtils.bookmarks.changeBookmarkURI(aId, uri);
}
});
deferred.resolve();
}.bind(this));
}.bind(this)
};
PlacesUtils.bookmarks.runInBatchMode(batch, null);
}
return deferred.promise;
},
/**
* Takes a JSON-serialized node and inserts it into the db.
*
* @param aData
* The unwrapped data blob of dropped or pasted data.
* @param aContainer
* The container the data was dropped or pasted into
* @param aIndex
* The index within the container the item was dropped or pasted at
* @return an array containing of maps of old folder ids to new folder ids,
* and an array of saved search ids that need to be fixed up.
* eg: [[[oldFolder1, newFolder1]], [search1]]
*/
importJSONNode: function BI_importJSONNode(aData, aContainer, aIndex,
aGrandParentId) {
return Task.spawn(function() {
let folderIdMap = [];
let searchIds = [];
let id = -1;
switch (aData.type) {
case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER:
if (aContainer == PlacesUtils.tagsFolderId) {
// Node is a tag
if (aData.children) {
aData.children.forEach(function(aChild) {
try {
PlacesUtils.tagging.tagURI(
NetUtil.newURI(aChild.uri), [aData.title]);
} catch (ex) {
// Invalid tag child, skip it
}
});
throw new Task.Result([folderIdMap, searchIds]);
}
} else if (aData.livemark && aData.annos) {
// Node is a livemark
let feedURI = null;
let siteURI = null;
aData.annos = aData.annos.filter(function(aAnno) {
switch (aAnno.name) {
case PlacesUtils.LMANNO_FEEDURI:
feedURI = NetUtil.newURI(aAnno.value);
return false;
case PlacesUtils.LMANNO_SITEURI:
siteURI = NetUtil.newURI(aAnno.value);
return false;
default:
return true;
}
});
if (feedURI) {
PlacesUtils.livemarks.addLivemark({
title: aData.title,
feedURI: feedURI,
parentId: aContainer,
index: aIndex,
lastModified: aData.lastModified,
siteURI: siteURI
}, function(aStatus, aLivemark) {
if (Components.isSuccessCode(aStatus)) {
let id = aLivemark.id;
if (aData.dateAdded)
PlacesUtils.bookmarks.setItemDateAdded(id, aData.dateAdded);
if (aData.annos && aData.annos.length)
PlacesUtils.setAnnotationsForItem(id, aData.annos);
}
});
}
} else {
id = PlacesUtils.bookmarks.createFolder(
aContainer, aData.title, aIndex);
folderIdMap[aData.id] = id;
// Process children
if (aData.children) {
for (let i = 0; i < aData.children.length; i++) {
let child = aData.children[i];
let [folders, searches] =
yield this.importJSONNode(child, id, i, aContainer);
for (let j = 0; j < folders.length; j++) {
if (folders[j])
folderIdMap[j] = folders[j];
}
searchIds = searchIds.concat(searches);
}
}
}
break;
case PlacesUtils.TYPE_X_MOZ_PLACE:
id = PlacesUtils.bookmarks.insertBookmark(
aContainer, NetUtil.newURI(aData.uri), aIndex, aData.title);
if (aData.keyword)
PlacesUtils.bookmarks.setKeywordForBookmark(id, aData.keyword);
if (aData.tags) {
let tags = aData.tags.split(", ");
if (tags.length)
PlacesUtils.tagging.tagURI(NetUtil.newURI(aData.uri), tags);
}
if (aData.charset) {
yield PlacesUtils.setCharsetForURI(
NetUtil.newURI(aData.uri), aData.charset);
}
if (aData.uri.substr(0, 6) == "place:")
searchIds.push(id);
if (aData.icon) {
try {
// Create a fake faviconURI to use (FIXME: bug 523932)
let faviconURI = NetUtil.newURI("fake-favicon-uri:" + aData.uri);
PlacesUtils.favicons.replaceFaviconDataFromDataURL(
faviconURI, aData.icon, 0);
PlacesUtils.favicons.setAndFetchFaviconForPage(
NetUtil.newURI(aData.uri), faviconURI, false,
PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE);
} catch (ex) {
Components.utils.reportError("Failed to import favicon data:" + ex);
}
}
if (aData.iconUri) {
try {
PlacesUtils.favicons.setAndFetchFaviconForPage(
NetUtil.newURI(aData.uri), NetUtil.newURI(aData.iconUri), false,
PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE);
} catch (ex) {
Components.utils.reportError("Failed to import favicon URI:" + ex);
}
}
break;
case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR:
id = PlacesUtils.bookmarks.insertSeparator(aContainer, aIndex);
break;
default:
// Unknown node type
}
// Set generic properties, valid for all nodes
if (id != -1 && aContainer != PlacesUtils.tagsFolderId &&
aGrandParentId != PlacesUtils.tagsFolderId) {
if (aData.dateAdded)
PlacesUtils.bookmarks.setItemDateAdded(id, aData.dateAdded);
if (aData.lastModified)
PlacesUtils.bookmarks.setItemLastModified(id, aData.lastModified);
if (aData.annos && aData.annos.length)
PlacesUtils.setAnnotationsForItem(id, aData.annos);
}
throw new Task.Result([folderIdMap, searchIds]);
}.bind(this));
}
}
function notifyObservers(topic) {
Services.obs.notifyObservers(null, topic, "json");
}
/**
* Replaces imported folder ids with their local counterparts in a place: URI.
*
* @param aURI
* A place: URI with folder ids.
* @param aFolderIdMap
* An array mapping old folder id to new folder ids.
* @returns the fixed up URI if all matched. If some matched, it returns
* the URI with only the matching folders included. If none matched
* it returns the input URI unchanged.
*/
function fixupQuery(aQueryURI, aFolderIdMap) {
let convert = function(str, p1, offset, s) {
return "folder=" + aFolderIdMap[p1];
}
let stringURI = aQueryURI.spec.replace(/folder=([0-9]+)/g, convert);
return NetUtil.newURI(stringURI);
}
function BookmarkExporter() {}
BookmarkExporter.prototype = {
exportToFile: function BE_exportToFile(aLocalFile) {
return Task.spawn(this._writeToFile(aLocalFile));
},
_converterOut: null,
_writeToFile: function BE__writeToFile(aLocalFile) {
// Create a file that can be accessed by the current user only.
let safeFileOut = Cc["@mozilla.org/network/safe-file-output-stream;1"].
createInstance(Ci.nsIFileOutputStream);
safeFileOut.init(aLocalFile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
FileUtils.MODE_TRUNCATE, parseInt("0600", 8), 0);
try {
// We need a buffered output stream for performance. See bug 202477.
let bufferedOut = Cc["@mozilla.org/network/buffered-output-stream;1"].
createInstance(Ci.nsIBufferedOutputStream);
bufferedOut.init(safeFileOut, 4096);
try {
// Write bookmarks in UTF-8.
this._converterOut = Cc["@mozilla.org/intl/converter-output-stream;1"].
createInstance(Ci.nsIConverterOutputStream);
this._converterOut.init(bufferedOut, "utf-8", 0, 0);
try {
yield this._writeContentToFile();
// Flush the buffer and retain the target file on success only.
bufferedOut.QueryInterface(Ci.nsISafeOutputStream).finish();
} finally {
this._converterOut.close();
this._converterOut = null;
}
} finally {
bufferedOut.close();
}
} finally {
safeFileOut.close();
}
},
_writeContentToFile: function BE__writeContentToFile() {
// Weep over stream interface variance.
let streamProxy = {
converter: this._converterOut,
write: function(aData, aLen) {
this.converter.writeString(aData);
}
};
// Get list of itemIds that must be excluded from the backup.
let excludeItems = PlacesUtils.annotations.getItemsWithAnnotation(
PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
let root = PlacesUtils.getFolderContents(PlacesUtils.placesRootId, false,
false).root;
// Serialize to JSON and write to stream.
yield BookmarkNode.serializeAsJSONToOutputStream(root, streamProxy, false, false,
excludeItems);
root.containerOpen = false;
}
}
let BookmarkNode = {
/**
* Serializes the given node (and all its descendents) as JSON
* and writes the serialization to the given output stream.
*
* @param aNode
* An nsINavHistoryResultNode
* @param aStream
* An nsIOutputStream. NOTE: it only uses the write(str, len)
* method of nsIOutputStream. The caller is responsible for
* closing the stream.
* @param aIsUICommand
* Boolean - If true, modifies serialization so that each node self-contained.
* For Example, tags are serialized inline with each bookmark.
* @param aResolveShortcuts
* Converts folder shortcuts into actual folders.
* @param aExcludeItems
* An array of item ids that should not be written to the backup.
* @returns Task promise
*/
serializeAsJSONToOutputStream: function BN_serializeAsJSONToOutputStream(
aNode, aStream, aIsUICommand, aResolveShortcuts, aExcludeItems) {
return Task.spawn(function() {
// Serialize to stream
let array = [];
if (yield this._appendConvertedNode(aNode, null, array, aIsUICommand,
aResolveShortcuts, aExcludeItems)) {
let json = JSON.stringify(array[0]);
aStream.write(json, json.length);
} else {
throw Cr.NS_ERROR_UNEXPECTED;
}
}.bind(this));
},
_appendConvertedNode: function BN__appendConvertedNode(
bNode, aIndex, aArray, aIsUICommand, aResolveShortcuts, aExcludeItems) {
return Task.spawn(function() {
let node = {};
// Set index in order received
// XXX handy shortcut, but are there cases where we don't want
// to export using the sorting provided by the query?
if (aIndex)
node.index = aIndex;
this._addGenericProperties(bNode, node, aResolveShortcuts);
let parent = bNode.parent;
let grandParent = parent ? parent.parent : null;
if (PlacesUtils.nodeIsURI(bNode)) {
// Tag root accept only folder nodes
if (parent && parent.itemId == PlacesUtils.tagsFolderId)
throw new Task.Result(false);
// Check for url validity, since we can't halt while writing a backup.
// This will throw if we try to serialize an invalid url and it does
// not make sense saving a wrong or corrupt uri node.
try {
NetUtil.newURI(bNode.uri);
} catch (ex) {
throw new Task.Result(false);
}
yield this._addURIProperties(bNode, node, aIsUICommand);
} else if (PlacesUtils.nodeIsContainer(bNode)) {
// Tag containers accept only uri nodes
if (grandParent && grandParent.itemId == PlacesUtils.tagsFolderId)
throw new Task.Result(false);
this._addContainerProperties(bNode, node, aIsUICommand,
aResolveShortcuts);
} else if (PlacesUtils.nodeIsSeparator(bNode)) {
// Tag root accept only folder nodes
// Tag containers accept only uri nodes
if ((parent && parent.itemId == PlacesUtils.tagsFolderId) ||
(grandParent && grandParent.itemId == PlacesUtils.tagsFolderId))
throw new Task.Result(false);
this._addSeparatorProperties(bNode, node);
}
if (!node.feedURI && node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
throw new Task.Result(yield this._appendConvertedComplexNode(node, bNode, aArray, aIsUICommand,
aResolveShortcuts, aExcludeItems));
}
aArray.push(node);
throw new Task.Result(true);
}.bind(this));
},
_addGenericProperties: function BN__addGenericProperties(
aPlacesNode, aJSNode, aResolveShortcuts) {
aJSNode.title = aPlacesNode.title;
aJSNode.id = aPlacesNode.itemId;
if (aJSNode.id != -1) {
let parent = aPlacesNode.parent;
if (parent)
aJSNode.parent = parent.itemId;
let dateAdded = aPlacesNode.dateAdded;
if (dateAdded)
aJSNode.dateAdded = dateAdded;
let lastModified = aPlacesNode.lastModified;
if (lastModified)
aJSNode.lastModified = lastModified;
// XXX need a hasAnnos api
let annos = [];
try {
annos =
PlacesUtils.getAnnotationsForItem(aJSNode.id).filter(function(anno) {
// XXX should whitelist this instead, w/ a pref for
// backup/restore of non-whitelisted annos
// XXX causes JSON encoding errors, so utf-8 encode
// anno.value = unescape(encodeURIComponent(anno.value));
if (anno.name == PlacesUtils.LMANNO_FEEDURI)
aJSNode.livemark = 1;
if (anno.name == PlacesUtils.READ_ONLY_ANNO && aResolveShortcuts) {
// When copying a read-only node, remove the read-only annotation.
return false;
}
return true;
});
} catch(ex) {}
if (annos.length != 0)
aJSNode.annos = annos;
}
// XXXdietrich - store annos for non-bookmark items
},
_addURIProperties: function BN__addURIProperties(
aPlacesNode, aJSNode, aIsUICommand) {
return Task.spawn(function() {
aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE;
aJSNode.uri = aPlacesNode.uri;
if (aJSNode.id && aJSNode.id != -1) {
// Harvest bookmark-specific properties
let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(aJSNode.id);
if (keyword)
aJSNode.keyword = keyword;
}
let tags = aIsUICommand ? aPlacesNode.tags : null;
if (tags)
aJSNode.tags = tags;
// Last character-set
let uri = NetUtil.newURI(aPlacesNode.uri);
let lastCharset = yield PlacesUtils.getCharsetForURI(uri)
if (lastCharset)
aJSNode.charset = lastCharset;
});
},
_addSeparatorProperties: function BN__addSeparatorProperties(
aPlacesNode, aJSNode) {
aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
},
_addContainerProperties: function BN__addContainerProperties(
aPlacesNode, aJSNode, aIsUICommand, aResolveShortcuts) {
let concreteId = PlacesUtils.getConcreteItemId(aPlacesNode);
if (concreteId != -1) {
// This is a bookmark or a tag container.
if (PlacesUtils.nodeIsQuery(aPlacesNode) ||
(concreteId != aPlacesNode.itemId && !aResolveShortcuts)) {
aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE;
aJSNode.uri = aPlacesNode.uri;
// Folder shortcut
if (aIsUICommand)
aJSNode.concreteId = concreteId;
} else {
// Bookmark folder or a shortcut we should convert to folder.
aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
// Mark root folders.
if (aJSNode.id == PlacesUtils.placesRootId)
aJSNode.root = "placesRoot";
else if (aJSNode.id == PlacesUtils.bookmarksMenuFolderId)
aJSNode.root = "bookmarksMenuFolder";
else if (aJSNode.id == PlacesUtils.tagsFolderId)
aJSNode.root = "tagsFolder";
else if (aJSNode.id == PlacesUtils.unfiledBookmarksFolderId)
aJSNode.root = "unfiledBookmarksFolder";
else if (aJSNode.id == PlacesUtils.toolbarFolderId)
aJSNode.root = "toolbarFolder";
}
} else {
// This is a grouped container query, generated on the fly.
aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE;
aJSNode.uri = aPlacesNode.uri;
}
},
_appendConvertedComplexNode: function BN__appendConvertedComplexNode(
aNode, aSourceNode, aArray, aIsUICommand, aResolveShortcuts,
aExcludeItems) {
return Task.spawn(function() {
let repr = {};
for (let [name, value] in Iterator(aNode))
repr[name] = value;
// Write child nodes
let children = repr.children = [];
if (!aNode.livemark) {
PlacesUtils.asContainer(aSourceNode);
let wasOpen = aSourceNode.containerOpen;
if (!wasOpen)
aSourceNode.containerOpen = true;
let cc = aSourceNode.childCount;
for (let i = 0; i < cc; ++i) {
let childNode = aSourceNode.getChild(i);
if (aExcludeItems && aExcludeItems.indexOf(childNode.itemId) != -1)
continue;
yield this._appendConvertedNode(aSourceNode.getChild(i), i, children,
aIsUICommand, aResolveShortcuts,
aExcludeItems);
}
if (!wasOpen)
aSourceNode.containerOpen = false;
}
aArray.push(repr);
throw new Task.Result(true);
}.bind(this));
}
}