Backed out changeset 4c465fe2d7c9 (bug 1095426)

This commit is contained in:
Sebastian Hengst 2017-06-16 19:12:18 +02:00
parent 57a332bfa6
commit 7ca69ab78f
12 changed files with 444 additions and 740 deletions

View File

@ -11,11 +11,11 @@ const Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource://gre/modules/PromiseUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
"resource://gre/modules/PlacesBackups.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
@ -243,85 +243,325 @@ BookmarkImporter.prototype = {
/**
* Import bookmarks from a JSON string.
*
* @param {String} aString JSON string of serialized bookmark data.
* @return {Promise}
* @resolves When the new bookmarks have been created.
* @rejects JavaScript exception.
* @param aString
* JSON string of serialized bookmark data.
*/
async importFromJSON(aString) {
this._importPromises = [];
let deferred = PromiseUtils.defer();
let nodes =
PlacesUtils.unwrapNodes(aString, PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER);
if (nodes.length == 0 || !nodes[0].children ||
nodes[0].children.length == 0) {
return;
deferred.resolve(); // Nothing to restore
} else {
// Ensure tag folder gets processed last
nodes[0].children.sort(function sortRoots(aNode, bNode) {
if (aNode.root && aNode.root == "tagsFolder")
return 1;
if (bNode.root && bNode.root == "tagsFolder")
return -1;
return 0;
});
let batch = {
nodes: nodes[0].children,
runBatched: () => {
if (this._replace) {
// 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.includes(childId) &&
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,
this._source);
} else {
PlacesUtils.bookmarks.removeItem(rootItemId, this._source);
}
}
}
let searchIds = [];
let folderIdMap = [];
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;
case "mobileFolder":
container = PlacesUtils.mobileFolderId;
break;
}
// Insert the data into the db
for (let child of node.children) {
let index = child.index;
let [folders, searches] =
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 {
let [folders, searches] = this.importJSONNode(
node, PlacesUtils.placesRootId, node.index, 0);
for (let i = 0; i < folders.length; i++) {
if (folders[i])
folderIdMap[i] = folders[i];
}
searchIds = searchIds.concat(searches);
}
}
// Fixup imported place: uris that contain folders
for (let id of searchIds) {
let oldURI = PlacesUtils.bookmarks.getBookmarkURI(id);
let uri = fixupQuery(oldURI, folderIdMap);
if (!uri.equals(oldURI)) {
PlacesUtils.bookmarks.changeBookmarkURI(id, uri, this._source);
}
}
deferred.resolve();
}
};
PlacesUtils.bookmarks.runInBatchMode(batch, null);
}
// Change to nodes[0].children as we don't import the root, and also filter
// out any obsolete "tagsFolder" sections.
nodes = nodes[0].children.filter(node => !node.root || node.root != "tagsFolder");
// If we're replacing, then erase existing bookmarks first.
if (this._replace) {
await PlacesBackups.eraseEverythingIncludingUserRoots({ source: this._source });
}
let folderIdToGuidMap = {};
let searchGuids = [];
// Now do some cleanup on the imported nodes so that the various guids
// match what we need for insertTree, and we also have mappings of folders
// so we can repair any searches after inserting the bookmarks (see bug 824502).
for (let node of nodes) {
if (!node.children || node.children.length == 0)
continue; // Nothing to restore for this root
// Ensure we set the source correctly.
node.source = this._source;
// Translate the node for insertTree.
let [folders, searches] = translateTreeTypes(node);
folderIdToGuidMap = Object.assign(folderIdToGuidMap, folders);
searchGuids = searchGuids.concat(searches);
}
// Now we can add the actual nodes to the database.
for (let node of nodes) {
// Drop any nodes without children, we can't insert them.
if (!node.children || node.children.length == 0) {
continue;
}
// Places is moving away from supporting user-defined folders at the top
// of the tree, however, until we have a migration strategy we need to
// ensure any non-built-in folders are created (xref bug 1310299).
if (!PlacesUtils.bookmarks.userContentRoots.includes(node.guid)) {
node.parentGuid = PlacesUtils.bookmarks.rootGuid;
await PlacesUtils.bookmarks.insert(node);
}
await PlacesUtils.bookmarks.insertTree(node);
// Now add any favicons.
try {
insertFaviconsForTree(node);
} catch (ex) {
Cu.reportError(`Failed to insert favicons: ${ex}`);
}
}
// Now update any bookmarks with a place: search that contain an index to
// a folder id.
for (let guid of searchGuids) {
let searchBookmark = await PlacesUtils.bookmarks.fetch(guid);
let url = await fixupQuery(searchBookmark.url, folderIdToGuidMap);
if (url != searchBookmark.url) {
await PlacesUtils.bookmarks.update({ guid, url, source: this._source });
}
await deferred.promise;
// TODO (bug 1095426) once converted to the new bookmarks API, methods will
// yield, so this hack should not be needed anymore.
try {
await Promise.all(this._importPromises);
} finally {
delete this._importPromises;
}
},
};
/**
* 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) {
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) {
for (let child of aData.children) {
try {
PlacesUtils.tagging.tagURI(
NetUtil.newURI(child.uri), [aData.title], this._source);
} catch (ex) {
// Invalid tag child, skip it
}
}
return [folderIdMap, searchIds];
}
} else if (aData.annos &&
aData.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI)) {
// 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) {
let lmPromise = PlacesUtils.livemarks.addLivemark({
title: aData.title,
feedURI,
parentId: aContainer,
index: aIndex,
lastModified: aData.lastModified,
siteURI,
guid: aData.guid,
source: this._source
}).then(aLivemark => {
let id = aLivemark.id;
if (aData.dateAdded)
PlacesUtils.bookmarks.setItemDateAdded(id, aData.dateAdded,
this._source);
if (aData.annos && aData.annos.length)
PlacesUtils.setAnnotationsForItem(id, aData.annos,
this._source);
});
this._importPromises.push(lmPromise);
}
} else {
let isMobileFolder = aData.annos &&
aData.annos.some(anno => anno.name == PlacesUtils.MOBILE_ROOT_ANNO);
if (isMobileFolder) {
// Mobile bookmark folders are special: we move their children to
// the mobile root instead of importing them. We also rewrite
// queries to use the special folder ID, and ignore generic
// properties like timestamps and annotations set on the folder.
id = PlacesUtils.mobileFolderId;
} else {
// For other folders, set `id` so that we can import timestamps
// and annotations at the end of this function.
id = PlacesUtils.bookmarks.createFolder(
aContainer, aData.title, aIndex, aData.guid, this._source);
}
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] =
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, aData.guid, this._source);
if (aData.keyword) {
// POST data could be set in 2 ways:
// 1. new backups have a postData property
// 2. old backups have an item annotation
let postDataAnno = aData.annos &&
aData.annos.find(anno => anno.name == PlacesUtils.POST_DATA_ANNO);
let postData = aData.postData || (postDataAnno && postDataAnno.value);
let kwPromise = PlacesUtils.keywords.insert({ keyword: aData.keyword,
url: aData.uri,
postData,
source: this._source });
this._importPromises.push(kwPromise);
}
if (aData.tags) {
let tags = aData.tags.split(",").filter(aTag =>
aTag.length <= Ci.nsITaggingService.MAX_TAG_LENGTH);
if (tags.length) {
try {
PlacesUtils.tagging.tagURI(NetUtil.newURI(aData.uri), tags, this._source);
} catch (ex) {
// Invalid tag child, skip it.
Cu.reportError(`Unable to set tags "${tags.join(", ")}" for ${aData.uri}: ${ex}`);
}
}
}
if (aData.charset) {
PlacesUtils.annotations.setPageAnnotation(
NetUtil.newURI(aData.uri), PlacesUtils.CHARSET_ANNO, aData.charset,
0, Ci.nsIAnnotationService.EXPIRE_NEVER);
}
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,
Services.scriptSecurityManager.getSystemPrincipal());
PlacesUtils.favicons.setAndFetchFaviconForPage(
NetUtil.newURI(aData.uri), faviconURI, false,
PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null,
Services.scriptSecurityManager.getSystemPrincipal());
} 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, null,
Services.scriptSecurityManager.getSystemPrincipal());
} 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, aData.guid, this._source);
break;
default:
// Unknown node type
}
// Set generic properties, valid for all nodes except tags and the mobile
// root.
if (id != -1 && id != PlacesUtils.mobileFolderId &&
aContainer != PlacesUtils.tagsFolderId &&
aGrandParentId != PlacesUtils.tagsFolderId) {
if (aData.dateAdded)
PlacesUtils.bookmarks.setItemDateAdded(id, aData.dateAdded,
this._source);
if (aData.lastModified)
PlacesUtils.bookmarks.setItemLastModified(id, aData.lastModified,
this._source);
if (aData.annos && aData.annos.length)
PlacesUtils.setAnnotationsForItem(id, aData.annos, this._source);
}
return [folderIdMap, searchIds];
}
}
function notifyObservers(topic) {
Services.obs.notifyObservers(null, topic, "json");
@ -330,227 +570,19 @@ function notifyObservers(topic) {
/**
* Replaces imported folder ids with their local counterparts in a place: URI.
*
* @param {nsIURI} aQueryURI
* @param aURI
* A place: URI with folder ids.
* @param {Object} aFolderIdMap
* An array mapping of old folder IDs to new folder GUIDs.
* @return {String} 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.
* @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.
*/
async function fixupQuery(aQueryURI, aFolderIdMap) {
const reGlobal = /folder=([0-9]+)/g;
const re = /([0-9]+)/;
// Unfortunately .replace can't handle async functions. Therefore,
// we find the folder guids we need to know the ids for first, then
// do the async request, and finally replace everything in one go.
let uri = aQueryURI.href;
let found = uri.match(reGlobal);
if (!found) {
return uri;
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);
let queryFolderGuids = [];
for (let folderString of found) {
let existingFolderId = folderString.match(re)[0];
queryFolderGuids.push(aFolderIdMap[existingFolderId])
}
let newFolderIds = await PlacesUtils.promiseManyItemIds(queryFolderGuids);
let convert = function(str, p1) {
return "folder=" + newFolderIds.get(aFolderIdMap[p1]);
}
return uri.replace(reGlobal, convert);
}
/**
* A mapping of root folder names to Guids. To help fixupRootFolderGuid.
*/
const rootToFolderGuidMap = {
"placesRoot": PlacesUtils.bookmarks.rootGuid,
"bookmarksMenuFolder": PlacesUtils.bookmarks.menuGuid,
"unfiledBookmarksFolder": PlacesUtils.bookmarks.unfiledGuid,
"toolbarFolder": PlacesUtils.bookmarks.toolbarGuid,
"mobileFolder": PlacesUtils.bookmarks.mobileGuid
};
/**
* Updates a bookmark node from the json version to the places GUID. This
* will only change GUIDs for the built-in folders. Other folders will remain
* unchanged.
*
* @param {Object} A bookmark node that is updated with the new GUID if necessary.
*/
function fixupRootFolderGuid(node) {
if (!node.guid && node.root && node.root in rootToFolderGuidMap) {
node.guid = rootToFolderGuidMap[node.root];
}
}
/**
* Translates the JSON types for a node and its children into Places compatible
* types. Also handles updating of other parameters e.g. dateAdded and lastModified.
*
* @param {Object} node A node to be updated. If it contains children, they will
* be updated as well.
* @return {Array} An array containing two items:
* - {Object} A map of current folder ids to GUIDS
* - {Array} An array of GUIDs for nodes that contain query URIs
*/
function translateTreeTypes(node) {
let folderIdToGuidMap = {};
let searchGuids = [];
// Do the uri fixup first, so we can be consistent in this function.
if (node.uri) {
node.url = node.uri;
delete node.uri;
}
switch (node.type) {
case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER:
node.type = PlacesUtils.bookmarks.TYPE_FOLDER;
// Older type mobile folders have a random guid with an annotation. We need
// to make sure those go into the proper mobile folder.
let isMobileFolder = node.annos &&
node.annos.some(anno => anno.name == PlacesUtils.MOBILE_ROOT_ANNO);
if (isMobileFolder) {
node.guid = PlacesUtils.bookmarks.mobileGuid;
} else {
// In case the Guid is broken, we need to fix it up.
fixupRootFolderGuid(node);
}
// Record the current id and the guid so that we can update any search
// queries later.
folderIdToGuidMap[node.id] = node.guid;
break;
case PlacesUtils.TYPE_X_MOZ_PLACE:
node.type = PlacesUtils.bookmarks.TYPE_BOOKMARK;
if (node.url && node.url.substr(0, 6) == "place:") {
searchGuids.push(node.guid);
}
break;
case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR:
node.type = PlacesUtils.bookmarks.TYPE_SEPARATOR;
if ("title" in node) {
delete node.title;
}
break;
default:
// TODO We should handle this in a more robust fashion, see bug 1373610.
Cu.reportError(`Unexpected bookmark type ${node.type}`);
break;
}
if (node.dateAdded) {
node.dateAdded = PlacesUtils.toDate(node.dateAdded);
}
if (node.lastModified) {
let lastModified = PlacesUtils.toDate(node.lastModified);
// Ensure we get a last modified date that's later or equal to the dateAdded
// so that we don't upset the Bookmarks API.
if (lastModified >= node.dataAdded) {
node.lastModified = lastModified;
} else {
delete node.lastModified;
}
}
if (node.tags) {
// Separate any tags into an array, and ignore any that are too long.
node.tags = node.tags.split(",").filter(aTag =>
aTag.length > 0 && aTag.length <= Ci.nsITaggingService.MAX_TAG_LENGTH);
// If we end up with none, then delete the property completely.
if (!node.tags.length) {
delete node.tags;
}
}
// Sometimes postData can be null, so delete it to make the validators happy.
if (node.postData == null) {
delete node.postData;
}
// Now handle any children.
if (!node.children) {
return [folderIdToGuidMap, searchGuids];
}
// First sort the children by index.
node.children = node.children.sort((a, b) => {
return a.index - b.index;
});
// Now do any adjustments required for the children.
for (let child of node.children) {
let [folders, searches] = translateTreeTypes(child);
folderIdToGuidMap = Object.assign(folderIdToGuidMap, folders);
searchGuids = searchGuids.concat(searches);
}
return [folderIdToGuidMap, searchGuids];
}
/**
* Handles inserting favicons into the database for a bookmark node.
* It is assumed the node has already been inserted into the bookmarks
* database.
*
* @param {Object} node The bookmark node for icons to be inserted.
*/
function insertFaviconForNode(node) {
if (node.icon) {
try {
// Create a fake faviconURI to use (FIXME: bug 523932)
let faviconURI = Services.io.newURI("fake-favicon-uri:" + node.url);
PlacesUtils.favicons.replaceFaviconDataFromDataURL(
faviconURI, node.icon, 0,
Services.scriptSecurityManager.getSystemPrincipal());
PlacesUtils.favicons.setAndFetchFaviconForPage(
Services.io.newURI(node.url), faviconURI, false,
PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null,
Services.scriptSecurityManager.getSystemPrincipal());
} catch (ex) {
Components.utils.reportError("Failed to import favicon data:" + ex);
}
}
if (!node.iconUri) {
return;
}
try {
PlacesUtils.favicons.setAndFetchFaviconForPage(
Services.io.newURI(node.url), Services.io.newURI(node.iconUri), false,
PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null,
Services.scriptSecurityManager.getSystemPrincipal());
} catch (ex) {
Components.utils.reportError("Failed to import favicon URI:" + ex);
}
}
/**
* Handles inserting favicons into the database for a bookmark tree - a node
* and its children.
*
* It is assumed the nodes have already been inserted into the bookmarks
* database.
*
* @param {Object} nodeTree The bookmark node tree for icons to be inserted.
*/
function insertFaviconsForTree(nodeTree) {
insertFaviconForNode(nodeTree);
if (nodeTree.children) {
for (let child of nodeTree.children) {
insertFaviconsForTree(child);
}
}
return NetUtil.newURI(stringURI);
}

View File

@ -150,12 +150,6 @@ var Bookmarks = Object.freeze({
// be removed. Do not rely on this, rather use the tagging service API.
tagsGuid: "tags________",
/**
* The GUIDs of the user content root folders that we support, for easy access
* as a set.
*/
userContentRoots: ["toolbar_____", "menu________", "unfiled_____", "mobile______"],
/**
* Inserts a bookmark-item into the bookmarks tree.
*
@ -269,7 +263,7 @@ var Bookmarks = Object.freeze({
* will be used for all the items inserted. Any indices or custom parentGuids
* set on children will be ignored and overwritten.
*
* @param {Object} tree
* @param tree
* object representing a tree of bookmark items to insert.
*
* @return {Promise} resolved when the creation is complete.
@ -287,7 +281,7 @@ var Bookmarks = Object.freeze({
}
if (!PlacesUtils.isValidGuid(tree.guid)) {
throw new Error(`The parent guid is not valid (${tree.guid} ${tree.title}).`);
throw new Error("The parent guid is not valid.");
}
if (tree.guid == this.rootGuid) {
@ -305,7 +299,6 @@ var Bookmarks = Object.freeze({
// Serialize the tree into an array of items to insert into the db.
let insertInfos = [];
let insertLivemarkInfos = [];
let urlsThatMightNeedPlaces = [];
// We want to use the same 'last added' time for all the entries
@ -363,14 +356,8 @@ var Bookmarks = Object.freeze({
(b.dateAdded && b.lastModified >= b.dateAdded) }
, index: { replaceWith: indexToUse++ }
, source: { replaceWith: source }
, annos: {}
, keyword: { validIf: b => b.type == TYPE_BOOKMARK }
, charset: { validIf: b => b.type == TYPE_BOOKMARK }
, postData: { validIf: b => b.type == TYPE_BOOKMARK }
, tags: { validIf: b => b.type == TYPE_BOOKMARK }
, children: { validIf: b => b.type == TYPE_FOLDER && Array.isArray(b.children) }
});
if (shouldUseNullIndices) {
insertInfo.index = null;
}
@ -379,21 +366,6 @@ var Bookmarks = Object.freeze({
if (insertInfo.type == Bookmarks.TYPE_BOOKMARK) {
urlsThatMightNeedPlaces.push(insertInfo.url);
}
// As we don't track indexes for children of root folders, and we
// insert livemarks separately, we create a temporary placeholder in
// the bookmarks, and later we'll replace it by the real livemark.
if (isLivemark(insertInfo)) {
// Make the current insertInfo item a placeholder.
let livemarkInfo = Object.assign({}, insertInfo);
// Delete the annotations that make it a livemark.
delete insertInfo.annos;
// Now save the livemark info for later.
insertLivemarkInfos.push(livemarkInfo);
}
insertInfos.push(insertInfo);
// Process any children. We have to use info.children here rather than
// insertInfo.children because validateBookmarkObject doesn't copy over
@ -417,7 +389,6 @@ var Bookmarks = Object.freeze({
}
return lastAddedForParent;
}
// We want to validate synchronously, but we can't know the index at which
// we're inserting into the parent. We just use NULL instead,
// and the SQL query with which we insert will update it as necessary.
@ -435,9 +406,6 @@ var Bookmarks = Object.freeze({
await insertBookmarkTree(insertInfos, source, parent,
urlsThatMightNeedPlaces, lastAddedForParent);
await insertLivemarkData(insertLivemarkInfos);
// Now update the indices of root items in the objects we return.
// These may be wrong if someone else modified the table between
// when we fetched the parent and inserted our items, but the actual
@ -458,32 +426,13 @@ var Bookmarks = Object.freeze({
let item = insertInfos[i];
let itemId = itemIdMap.get(item.guid);
let uri = item.hasOwnProperty("url") ? PlacesUtils.toURI(item.url) : null;
// For sub-folders, we need to make sure their children have the correct parent ids.
let parentId;
if (item.guid === parent.guid ||
Bookmarks.userContentRoots.includes(item.parentGuid)) {
// We're the item being inserted at the top-level, or we're a top-level
// folder, so the parent id won't have changed.
parentId = parent._id;
} else {
// This is a parent folder that's been updated, so we need to
// use the new item id.
parentId = itemIdMap.get(item.parentGuid);
}
notify(observers, "onItemAdded", [ itemId, parentId, item.index,
notify(observers, "onItemAdded", [ itemId, parent._id, item.index,
item.type, uri, item.title || null,
PlacesUtils.toPRTime(item.dateAdded), item.guid,
item.parentGuid, item.source ],
{ isTagging: false });
// Remove non-enumerable properties.
delete item.source;
// Note, annotations for livemark data are deleted from insertInfo
// within appendInsertionInfoForInfoArray, so we won't be duplicating
// the insertions here.
await handleBookmarkItemSpecialData(itemId, item);
insertInfos[i] = Object.assign({}, item);
}
return insertInfos;
@ -749,16 +698,18 @@ var Bookmarks = Object.freeze({
options.source = Bookmarks.SOURCES.DEFAULT;
}
const folderGuids = [this.toolbarGuid, this.menuGuid, this.unfiledGuid,
this.mobileGuid];
return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: eraseEverything",
async function(db) {
let urls;
await db.executeTransaction(async function() {
urls = await removeFoldersContents(db, Bookmarks.userContentRoots, options);
urls = await removeFoldersContents(db, folderGuids, options);
const time = PlacesUtils.toPRTime(new Date());
const syncChangeDelta =
PlacesSyncUtils.bookmarks.determineSyncChangeDelta(options.source);
for (let folderGuid of Bookmarks.userContentRoots) {
for (let folderGuid of folderGuids) {
await db.executeCached(
`UPDATE moz_bookmarks SET lastModified = :time,
syncChangeCounter = syncChangeCounter + :syncChangeDelta
@ -1366,16 +1317,6 @@ function insertBookmark(item, parent) {
});
}
/**
* Determines if a bookmark is a Livemark depending on how it is annotated.
*
* @param {Object} node The bookmark node to check.
* @returns {Boolean} True if the node is a Livemark, false otherwise.
*/
function isLivemark(node) {
return node.annos && node.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI);
}
function insertBookmarkTree(items, source, parent, urls, lastAddedForParent) {
return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: insertBookmarkTree", async function(db) {
await db.executeTransaction(async function transaction() {
@ -1416,102 +1357,6 @@ function insertBookmarkTree(items, source, parent, urls, lastAddedForParent) {
});
}
/**
* Handles any Livemarks within the passed items.
*
* @param {Array} items Livemark items that need to be added.
*/
async function insertLivemarkData(items) {
for (let item of items) {
let feedURI = null;
let siteURI = null;
item.annos = item.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;
}
});
let index = null;
// Delete the placeholder but note the index of it, so that we
// can insert the livemark item at the right place.
let placeholder = await Bookmarks.fetch(item.guid);
index = placeholder.index;
await Bookmarks.remove(item.guid, {source: item.source});
if (feedURI) {
item.feedURI = feedURI;
item.siteURI = siteURI;
item.index = index;
if (item.dateAdded) {
item.dateAdded = PlacesUtils.toPRTime(item.dateAdded);
}
if (item.lastModified) {
item.lastModified = PlacesUtils.toPRTime(item.lastModified);
}
let livemark = await PlacesUtils.livemarks.addLivemark(item);
let id = livemark.id;
if (item.annos && item.annos.length) {
PlacesUtils.setAnnotationsForItem(id, item.annos,
item.source);
}
}
}
}
/**
* Handles special data on a bookmark, e.g. annotations, keywords, tags, charsets,
* inserting the data into the appropriate place.
*
* @param {Integer} itemId The ID of the item within the bookmarks database.
* @param {Object} item The bookmark item with possible special data to be inserted.
*/
async function handleBookmarkItemSpecialData(itemId, item) {
if (item.annos && item.annos.length) {
PlacesUtils.setAnnotationsForItem(itemId, item.annos, item.source)
}
if ("keyword" in item && item.keyword) {
// POST data could be set in 2 ways:
// 1. new backups have a postData property
// 2. old backups have an item annotation
let postDataAnno = item.annos &&
item.annos.find(anno => anno.name == PlacesUtils.POST_DATA_ANNO);
let postData = item.postData || (postDataAnno && postDataAnno.value);
try {
await PlacesUtils.keywords.insert({
keyword: item.keyword,
url: item.url,
postData,
source: item.source
});
} catch (ex) {
Cu.reportError(`Failed to insert keywords: ${ex}`);
}
}
if ("tags" in item) {
try {
PlacesUtils.tagging.tagURI(NetUtil.newURI(item.url), item.tags, item._source);
} catch (ex) {
// Invalid tag child, skip it.
Cu.reportError(`Unable to set tags "${item.tags.join(", ")}" for ${item.url}: ${ex}`);
}
}
if ("charset" in item && item.charset) {
await PlacesUtils.setCharsetForURI(NetUtil.newURI(item.url), item.charset);
}
}
// Query implementation.
async function queryBookmarks(info) {

View File

@ -81,30 +81,6 @@ function getBackupFileForSameDate(aFilename) {
})();
}
/**
* Returns the top-level bookmark folders ids and guids.
*
* @return {Promise} Resolve with an array of objects containing id and guid
* when the query is complete.
*/
async function getTopLevelFolderIds() {
let db = await PlacesUtils.promiseDBConnection();
let rows = await db.execute(
"SELECT id, guid FROM moz_bookmarks WHERE parent = :parentId",
{ parentId: PlacesUtils.placesRootId }
);
let guids = [];
for (let row of rows) {
guids.push({
id: row.getResultByName("id"),
guid: row.getResultByName("guid")
});
}
return guids;
}
this.PlacesBackups = {
/**
* Matches the backup filename:
@ -568,44 +544,6 @@ this.PlacesBackups = {
Components.utils.reportError("Unable to report telemetry.");
}
return [root, root.itemsCount];
},
/**
* Wrapper for PlacesUtils.bookmarks.eraseEverything that removes non-default
* roots.
*
* Note that default roots are preserved, only their children will be removed.
*
* TODO Ideally we wouldn't need to worry about non-default roots. However,
* until bug 1310299 is fixed, we still need to manage them.
*
* @param {Object} [options={}]
* Additional options. Currently supports the following properties:
* - source: The change source, forwarded to all bookmark observers.
* Defaults to nsINavBookmarksService::SOURCE_DEFAULT.
*
* @return {Promise} resolved when the removal is complete.
* @resolves once the removal is complete.
*/
async eraseEverythingIncludingUserRoots(options = {}) {
if (!options.source) {
options.source = PlacesUtils.bookmarks.SOURCES.DEFAULT;
}
let excludeItems =
PlacesUtils.annotations.getItemsWithAnnotation(PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
let rootFolderChildren = await getTopLevelFolderIds();
// We only need to do top-level roots here.
for (let child of rootFolderChildren) {
if (!PlacesUtils.bookmarks.userContentRoots.includes(child.guid) &&
child.guid != PlacesUtils.bookmarks.tagsGuid &&
!excludeItems.includes(child.id)) {
await PlacesUtils.bookmarks.remove(child.guid, {source: options.source});
}
}
return PlacesUtils.bookmarks.eraseEverything(options);
},
}
}

View File

@ -249,11 +249,6 @@ const BOOKMARK_VALIDATORS = Object.freeze({
},
source: simpleValidateFunc(v => Number.isInteger(v) &&
Object.values(PlacesUtils.bookmarks.SOURCES).includes(v)),
annos: simpleValidateFunc(v => Array.isArray(v) && v.length),
keyword: simpleValidateFunc(v => (typeof(v) == "string") && v.length),
charset: simpleValidateFunc(v => (typeof(v) == "string") && v.length),
postData: simpleValidateFunc(v => (typeof(v) == "string") && v.length),
tags: simpleValidateFunc(v => Array.isArray(v) && v.length),
});
// Sync bookmark records can contain additional properties.
@ -1579,8 +1574,8 @@ this.PlacesUtils = {
/**
* Sets the character-set for a URI.
*
* @param {nsIURI} aURI
* @param {String} aCharset character-set value.
* @param aURI nsIURI
* @param aCharset character-set value.
* @return {Promise}
*/
setCharsetForURI: function PU_setCharsetForURI(aURI, aCharset) {
@ -1728,22 +1723,13 @@ this.PlacesUtils = {
* @param aGuid
* an item GUID
* @return {Promise}
* @resolves to the item id.
* @resolves to the GUID.
* @rejects if there's no item for the given GUID.
*/
promiseItemId(aGuid) {
return GuidHelper.getItemId(aGuid)
},
/**
* Get the item ids for multiple items (a bookmark, a folder or a separator)
* given the unique ids for each item.
*
* @param {Array} aGuids An array of item GUIDs.
* @return {Promise}
* @resolves to a Map of item ids.
* @rejects if not all of the GUIDs could be found.
*/
promiseManyItemIds(aGuids) {
return GuidHelper.getManyItemIds(aGuids);
},

View File

@ -40,121 +40,65 @@ const DEFAULT_INDEX = PlacesUtils.bookmarks.DEFAULT_INDEX;
var test = {
_testRootId: null,
_testRootTitle: "test root",
_folderGuids: [],
_folderIds: [],
_bookmarkURIs: [],
_count: 3,
_extraBookmarksCount: 10,
populate: async function populate() {
populate: function populate() {
// folder to hold this test
await PlacesUtils.bookmarks.eraseEverything();
let testFolderItems = [];
// Set a date 60 seconds ago, so that we can set newer bookmarks later.
let dateAdded = new Date(new Date() - 60000);
PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.toolbarFolderId);
this._testRootId =
PlacesUtils.bookmarks.createFolder(PlacesUtils.toolbarFolderId,
this._testRootTitle, DEFAULT_INDEX);
// create test folders each with a bookmark
for (let i = 0; i < this._count; i++) {
this._folderGuids.push(PlacesUtils.history.makeGuid());
testFolderItems.push({
guid: this._folderGuids[i],
title: `folder${i}`,
type: PlacesUtils.bookmarks.TYPE_FOLDER,
dateAdded,
children: [{
dateAdded,
url: `http://${i}`,
title: `bookmark${i}`,
}]
});
for (var i = 0; i < this._count; i++) {
var folderId =
PlacesUtils.bookmarks.createFolder(this._testRootId, "folder" + i, DEFAULT_INDEX);
this._folderIds.push(folderId)
var bookmarkURI = uri("http://" + i);
PlacesUtils.bookmarks.insertBookmark(folderId, bookmarkURI,
DEFAULT_INDEX, "bookmark" + i);
this._bookmarkURIs.push(bookmarkURI);
}
let bookmarksTree = {
guid: PlacesUtils.bookmarks.toolbarGuid,
children: [{
dateAdded,
title: this._testRootTitle,
type: PlacesUtils.bookmarks.TYPE_FOLDER,
children: testFolderItems
}]
};
let insertedBookmarks = await PlacesUtils.bookmarks.insertTree(bookmarksTree);
// create a query URI with 1 folder (ie: folder shortcut)
let folderIdsMap = await PlacesUtils.promiseManyItemIds(this._folderGuids);
let folderIds = [];
for (let id of folderIdsMap.values()) {
folderIds.push(id);
}
this._queryURI1 = `place:folder=${folderIdsMap.get(this._folderGuids[0])}&queryType=1`;
this._queryURI1 = uri("place:folder=" + this._folderIds[0] + "&queryType=1");
this._queryTitle1 = "query1";
await PlacesUtils.bookmarks.insert({
parentGuid: insertedBookmarks[0].guid,
dateAdded,
url: this._queryURI1,
title: this._queryTitle1
});
PlacesUtils.bookmarks.insertBookmark(this._testRootId, this._queryURI1,
DEFAULT_INDEX, this._queryTitle1);
// create a query URI with _count folders
this._queryURI2 = `place:folder=${folderIds.join("&folder=")}&queryType=1`;
this._queryURI2 = uri("place:folder=" + this._folderIds.join("&folder=") + "&queryType=1");
this._queryTitle2 = "query2";
await PlacesUtils.bookmarks.insert({
parentGuid: insertedBookmarks[0].guid,
dateAdded,
url: this._queryURI2,
title: this._queryTitle2
});
PlacesUtils.bookmarks.insertBookmark(this._testRootId, this._queryURI2,
DEFAULT_INDEX, this._queryTitle2);
// create a query URI with _count queries (each with a folder)
// first get a query object for each folder
var queries = folderIds.map(function(aFolderId) {
var queries = this._folderIds.map(function(aFolderId) {
var query = PlacesUtils.history.getNewQuery();
query.setFolders([aFolderId], 1);
return query;
});
var options = PlacesUtils.history.getNewQueryOptions();
options.queryType = options.QUERY_TYPE_BOOKMARKS;
this._queryURI3 =
PlacesUtils.history.queriesToQueryString(queries, queries.length, options);
uri(PlacesUtils.history.queriesToQueryString(queries, queries.length, options));
this._queryTitle3 = "query3";
await PlacesUtils.bookmarks.insert({
parentGuid: insertedBookmarks[0].guid,
dateAdded,
url: this._queryURI3,
title: this._queryTitle3
});
// Create a query URI for most recent bookmarks with NO folders specified.
this._queryURI4 = "place:queryType=1&sort=12&excludeItemIfParentHasAnnotation=livemark%2FfeedURI&maxResults=10&excludeQueries=1";
this._queryTitle4 = "query4";
await PlacesUtils.bookmarks.insert({
parentGuid: insertedBookmarks[0].guid,
dateAdded,
url: this._queryURI4,
title: this._queryTitle4
});
dump_table("moz_bookmarks");
dump_table("moz_places");
PlacesUtils.bookmarks.insertBookmark(this._testRootId, this._queryURI3,
DEFAULT_INDEX, this._queryTitle3);
},
clean() {},
validate: async function validate(addExtras) {
if (addExtras) {
// Throw a wrench in the works by inserting some new bookmarks,
// ensuring folder ids won't be the same, when restoring.
let date = new Date() - (this._extraBookmarksCount * 1000);
for (let i = 0; i < this._extraBookmarksCount; i++) {
await PlacesUtils.bookmarks.insert({
parentGuid: PlacesUtils.bookmarks.menuGuid,
url: uri("http://aaaa" + i),
dateAdded: new Date(date + ((this._extraBookmarksCount - i) * 1000)),
});
}
validate: function validate() {
// Throw a wrench in the works by inserting some new bookmarks,
// ensuring folder ids won't be the same, when restoring.
for (let i = 0; i < 10; i++) {
PlacesUtils.bookmarks.
insertBookmark(PlacesUtils.bookmarksMenuFolderId, uri("http://aaaa" + i), DEFAULT_INDEX, "");
}
var toolbar =
@ -168,8 +112,8 @@ var test = {
folderNode.QueryInterface(Ci.nsINavHistoryQueryResultNode);
folderNode.containerOpen = true;
// |_count| folders + the query nodes
do_check_eq(folderNode.childCount, this._count + 4);
// |_count| folders + the query node
do_check_eq(folderNode.childCount, this._count + 3);
for (let i = 0; i < this._count; i++) {
var subFolder = folderNode.getChild(i);
@ -191,9 +135,6 @@ var test = {
// validate multiple queries query
this.validateQueryNode3(folderNode.getChild(this._count + 2));
// validate recent folders query
this.validateQueryNode4(folderNode.getChild(this._count + 3));
// clean up
folderNode.containerOpen = false;
toolbar.containerOpen = false;
@ -207,8 +148,8 @@ var test = {
aNode.containerOpen = true;
do_check_eq(aNode.childCount, 1);
var child = aNode.getChild(0);
do_check_true(uri(child.uri).equals(uri("http://0")));
do_check_eq(child.title, "bookmark0");
do_check_true(uri(child.uri).equals(uri("http://0")))
do_check_eq(child.title, "bookmark0")
aNode.containerOpen = false;
},
@ -240,51 +181,40 @@ var test = {
do_check_eq(child.title, "bookmark" + i)
}
aNode.containerOpen = false;
},
validateQueryNode4(aNode) {
do_check_eq(aNode.title, this._queryTitle4);
do_check_true(PlacesUtils.nodeIsQuery(aNode));
aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
aNode.containerOpen = true;
// The query will list the extra bookmarks added at the start of validate.
do_check_eq(aNode.childCount, this._extraBookmarksCount);
for (var i = 0; i < aNode.childCount; i++) {
var child = aNode.getChild(i);
do_check_eq(child.uri, `http://aaaa${i}/`);
}
aNode.containerOpen = false;
},
}
}
tests.push(test);
function run_test() {
run_next_test();
}
add_task(async function() {
// make json file
let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json");
// populate db
for (let singleTest of tests) {
await singleTest.populate();
tests.forEach(function(aTest) {
aTest.populate();
// sanity
await singleTest.validate(true);
}
aTest.validate();
});
// export json to file
await BookmarkJSONUtils.exportToFile(jsonFile);
// clean
for (let singleTest of tests) {
singleTest.clean();
}
tests.forEach(function(aTest) {
aTest.clean();
});
// restore json file
await BookmarkJSONUtils.importFromFile(jsonFile, true);
// validate
for (let singleTest of tests) {
await singleTest.validate(false);
}
tests.forEach(function(aTest) {
aTest.validate();
});
// clean up
await OS.File.remove(jsonFile);

View File

@ -4,6 +4,7 @@
* 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/. */
const EXCLUDE_FROM_BACKUP_ANNO = "places/excludeFromBackup";
// Menu, Toolbar, Unsorted, Tags, Mobile
const PLACES_ROOTS_COUNT = 5;
var tests = [];
@ -48,7 +49,7 @@ var test = {
idx, "exclude uri");
// Annotate the bookmark for exclusion.
PlacesUtils.annotations.setItemAnnotation(exItemId,
PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1, 0,
EXCLUDE_FROM_BACKUP_ANNO, 1, 0,
PlacesUtils.annotations.EXPIRE_NEVER);
// create a root to be exclude
@ -58,7 +59,7 @@ var test = {
this._excludeRootTitle, idx);
// Annotate the root for exclusion.
PlacesUtils.annotations.setItemAnnotation(this._excludeRootId,
PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1, 0,
EXCLUDE_FROM_BACKUP_ANNO, 1, 0,
PlacesUtils.annotations.EXPIRE_NEVER);
// add a test bookmark exclude by exclusion of its parent
PlacesUtils.bookmarks.insertBookmark(this._excludeRootId,
@ -104,30 +105,26 @@ var test = {
}
}
// make json file
var jsonFile;
function run_test() {
run_next_test();
}
add_task(async function setup() {
jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json");
});
add_task(async function() {
// make json file
let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json");
add_task(async function test_export_import_excluded_file() {
// populate db
test.populate();
await BookmarkJSONUtils.exportToFile(jsonFile);
// restore json file
do_print("Restoring json file");
await BookmarkJSONUtils.importFromFile(jsonFile, true);
// validate without removing all bookmarks
// restore do not remove backup exclude entries
do_print("Validating...");
test.validate(false);
});
add_task(async function test_clearing_then_importing() {
// cleanup
await PlacesUtils.bookmarks.eraseEverything();
// manually remove the excluded root

View File

@ -20,9 +20,6 @@ this.push(myTest);
*/
tests.push({
// Initialise something to avoid undefined property warnings in validate.
_litterTitle: "",
populate: function populate() {
// check initial size
var rootNode = PlacesUtils.getFolderContents(PlacesUtils.placesRootId,
@ -122,6 +119,10 @@ tests.push({
}
});
function run_test() {
run_next_test();
}
add_task(async function() {
// make json file
let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json");

View File

@ -49,18 +49,6 @@
"uri": "http://en-us.www.mozilla.com/en-US/firefox/customize/",
"icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
},
{
"guid": "OCyeUO5uu9FJ",
"index": 3,
"title": "About Us",
"id": 10,
"parent": 6,
"dateAdded": 1361551979376699,
"lastModified": 1361551979379060,
"type": "text/x-moz-place",
"uri": "http://en-us.www.mozilla.com/en-US/about/",
"icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
},
{
"guid": "OCyeUO5uu9FI",
"index": 2,
@ -74,24 +62,16 @@
"icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
},
{
"guid": "QFM-QnE2ZpMz",
"title": "Test null postData",
"index": 4,
"dateAdded": 1481639510868000,
"lastModified": 1489563704300000,
"id": 17,
"charset": "UTF-8",
"annos": [
{
"name": "bookmarkProperties/description",
"flags": 0,
"expires": 4,
"value": "The best"
}
],
"guid": "OCyeUO5uu9FJ",
"index": 3,
"title": "About Us",
"id": 10,
"parent": 6,
"dateAdded": 1361551979376699,
"lastModified": 1361551979379060,
"type": "text/x-moz-place",
"uri": "http://example.com/search?q=%s&suggid=",
"postData": null
"uri": "http://en-us.www.mozilla.com/en-US/about/",
"icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
}
]
},

View File

@ -11,6 +11,10 @@ var bookmarkData = [
{ uri: uri("http://en.wikipedia.org/wiki/Diplodocus"), title: "dinosaur, dj, rad word" }
];
function run_test() {
run_next_test();
}
/*
HTML+FEATURES SUMMARY:
- import legacy bookmarks
@ -48,7 +52,7 @@ add_task(async function() {
title });
}
await validate("initial database");
await validate();
// Test exporting a Places canonical json file.
// 1. export to bookmarks.exported.json
@ -61,18 +65,14 @@ add_task(async function() {
do_print("imported json");
// 4. run the test-suite
await validate("re-imported json");
await validate();
do_print("validated import");
});
async function validate(infoMsg) {
do_print(`Validating ${infoMsg}: testMenuBookmarks`);
async function validate() {
await testMenuBookmarks();
do_print(`Validating ${infoMsg}: testToolbarBookmarks`);
await testToolbarBookmarks();
do_print(`Validating ${infoMsg}: testUnfiledBookmarks`);
testUnfiledBookmarks();
do_print(`Validating ${infoMsg}: testTags`);
testTags();
await PlacesTestUtils.promiseAsyncUpdates();
}

View File

@ -36,10 +36,6 @@ var test_bookmarks = {
title: "About Us",
url: "http://en-us.www.mozilla.com/en-US/about/",
icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
},
{ guid: "QFM-QnE2ZpMz",
title: "Test null postData",
url: "http://example.com/search?q=%s&suggid="
}
]
},
@ -76,10 +72,7 @@ var test_bookmarks = {
{ guid: "OCyeUO5uu9FR",
title: "Latest Headlines",
url: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/",
feedUrl: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml",
// Note: date gets truncated to milliseconds, whereas the value in bookmarks.json
// has full microseconds.
dateAdded: 1361551979451000,
feedUrl: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml"
}
],
unfiled: [
@ -199,7 +192,7 @@ function checkItem(aExpected, aNode) {
let data = await deferred.promise;
let base64Icon = "data:image/png;base64," +
base64EncodeString(String.fromCharCode.apply(String, data));
do_check_eq(base64Icon, aExpected.icon);
do_check_true(base64Icon == aExpected.icon);
break;
case "keyword": {
let entry = await PlacesUtils.keywords.fetch({ url: aNode.uri });

View File

@ -77,8 +77,8 @@ add_task(async function test_import_mobile_bookmarks_root() {
guid: PlacesUtils.bookmarks.menuGuid,
index: 0,
children: [
{ guid: "X6lUyOspVYwi", index: 0 },
{ guid: "Utodo9b0oVws", index: 1 },
{ guid: "Utodo9b0oVws", index: 0 },
{ guid: "X6lUyOspVYwi", index: 1 },
],
}, {
guid: PlacesUtils.bookmarks.toolbarGuid,
@ -96,12 +96,10 @@ add_task(async function test_import_mobile_bookmarks_root() {
value: 1,
}],
children: [
// The first two are in ..._import.json, the second two are in
// ..._merge.json
{ guid: "_o8e1_zxTJFg", index: 0 },
{ guid: "QCtSqkVYUbXB", index: 1 },
{ guid: "a17yW6-nTxEJ", index: 2 },
{ guid: "xV10h9Wi3FBM", index: 3 },
{ guid: "a17yW6-nTxEJ", index: 0 },
{ guid: "xV10h9Wi3FBM", index: 1 },
{ guid: "_o8e1_zxTJFg", index: 2 },
{ guid: "QCtSqkVYUbXB", index: 3 },
],
}],
}, "Should merge bookmarks root contents");
@ -171,9 +169,9 @@ add_task(async function test_import_mobile_bookmarks_folder() {
guid: PlacesUtils.bookmarks.menuGuid,
index: 0,
children: [
{ guid: "X6lUyOspVYwi", index: 0 },
{ guid: "XF4yRP6bTuil", index: 1 },
{ guid: "Utodo9b0oVws", index: 2 },
{ guid: "Utodo9b0oVws", index: 0 },
{ guid: "X6lUyOspVYwi", index: 1 },
{ guid: "XF4yRP6bTuil", index: 2 },
],
}, {
guid: PlacesUtils.bookmarks.toolbarGuid,
@ -193,10 +191,10 @@ add_task(async function test_import_mobile_bookmarks_folder() {
value: 1,
}],
children: [
{ guid: "_o8e1_zxTJFg", index: 0 },
{ guid: "QCtSqkVYUbXB", index: 1 },
{ guid: "a17yW6-nTxEJ", index: 2 },
{ guid: "xV10h9Wi3FBM", index: 3 },
{ guid: "a17yW6-nTxEJ", index: 0 },
{ guid: "xV10h9Wi3FBM", index: 1 },
{ guid: "_o8e1_zxTJFg", index: 2 },
{ guid: "QCtSqkVYUbXB", index: 3 },
],
}],
}, "Should merge bookmarks folder contents into mobile root");
@ -237,8 +235,8 @@ add_task(async function test_restore_multiple_bookmarks_folders() {
value: 1,
}],
children: [
{ guid: "a17yW6-nTxEJ", index: 0 },
{ guid: "sSZ86WT9WbN3", index: 1 },
{ guid: "sSZ86WT9WbN3", index: 0 },
{ guid: "a17yW6-nTxEJ", index: 1 },
],
}],
}, "Should restore multiple bookmarks folder contents into root");
@ -259,10 +257,10 @@ add_task(async function test_import_multiple_bookmarks_folders() {
guid: PlacesUtils.bookmarks.menuGuid,
index: 0,
children: [
{ guid: "X6lUyOspVYwi", index: 0 },
{ guid: "buy7711R3ZgE", index: 1 },
{ guid: "F_LBgd1fS_uQ", index: 2 },
{ guid: "oIpmQXMWsXvY", index: 3 },
{ guid: "buy7711R3ZgE", index: 0 },
{ guid: "F_LBgd1fS_uQ", index: 1 },
{ guid: "oIpmQXMWsXvY", index: 2 },
{ guid: "X6lUyOspVYwi", index: 3 },
],
}, {
guid: PlacesUtils.bookmarks.toolbarGuid,
@ -282,10 +280,10 @@ add_task(async function test_import_multiple_bookmarks_folders() {
value: 1,
}],
children: [
{ guid: "_o8e1_zxTJFg", index: 0 },
{ guid: "QCtSqkVYUbXB", index: 1 },
{ guid: "a17yW6-nTxEJ", index: 2 },
{ guid: "sSZ86WT9WbN3", index: 3 },
{ guid: "sSZ86WT9WbN3", index: 0 },
{ guid: "a17yW6-nTxEJ", index: 1 },
{ guid: "_o8e1_zxTJFg", index: 2 },
{ guid: "QCtSqkVYUbXB", index: 3 },
],
}],
}, "Should merge multiple mobile folders into root");

View File

@ -6,13 +6,17 @@ const {
fetchGuidsWithAnno,
} = Cu.import("resource://gre/modules/PlacesSyncUtils.jsm", {});
Cu.import("resource://testing-common/httpd.js");
Cu.importGlobalProperties(["URLSearchParams"]);
Cu.importGlobalProperties(["crypto", "URLSearchParams"]);
const DESCRIPTION_ANNO = "bookmarkProperties/description";
const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
const SYNC_PARENT_ANNO = "sync/parent";
var makeGuid = PlacesUtils.history.makeGuid;
function makeGuid() {
return ChromeUtils.base64URLEncode(crypto.getRandomValues(new Uint8Array(9)), {
pad: false,
});
}
function makeLivemarkServer() {
let server = new HttpServer();