--- a/services/sync/tests/unit/test_bookmark_engine.js
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -558,16 +558,20 @@ add_task(async function test_sync_dateAd
await PlacesSyncUtils.bookmarks.reset();
let engine = new BookmarksEngine(Service);
let store = engine._store;
let server = serverForFoo(engine);
await SyncTestingInfrastructure(server);
let collection = server.user("foo").collection("bookmarks");
+ // TODO: Avoid random orange (bug 1374599), this is only necessary
+ // intermittently - reset the last sync date so that we'll get all bookmarks.
+ engine.lastSync = 1;
+
Svc.Obs.notify("weave:engine:start-tracking"); // We skip usual startup...
// Just matters that it's in the past, not how far.
let now = Date.now();
let oneYearMS = 365 * 24 * 60 * 60 * 1000;
try {
let item1GUID = "abcdefabcdef";
--- a/toolkit/components/places/BookmarkJSONUtils.jsm
+++ b/toolkit/components/places/BookmarkJSONUtils.jsm
@@ -6,21 +6,21 @@ this.EXPORTED_SYMBOLS = [ "BookmarkJSONU
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
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",
"resource://gre/modules/Deprecated.jsm");
XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => new TextDecoder());
XPCOMUtils.defineLazyGetter(this, "gTextEncoder", () => new TextEncoder());
@@ -238,351 +238,319 @@ BookmarkImporter.prototype = {
converter.charset = "UTF-8";
let jsonString = converter.convertFromByteArray(aResult, aResult.length);
await this.importFromJSON(jsonString);
},
/**
* Import bookmarks from a JSON string.
*
- * @param aString
- * JSON string of serialized bookmark data.
+ * @param {String} aString JSON string of serialized bookmark data.
+ * @return {Promise}
+ * @resolves When the new bookmarks have been created.
+ * @rejects JavaScript exception.
*/
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) {
- 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;
- });
+ return;
+ }
+
+ // 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 = [];
- 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;
+ // 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
- 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);
- }
- }
- }
+ // Ensure we set the source correctly.
+ node.source = this._source;
- let searchIds = [];
- let folderIdMap = [];
+ // Translate the node for insertTree.
+ let [folders, searches] = translateTreeTypes(node);
- for (let node of batch.nodes) {
- if (!node.children || node.children.length == 0)
- continue; // Nothing to restore for this root
+ folderIdToGuidMap = Object.assign(folderIdToGuidMap, folders);
+ searchGuids = searchGuids.concat(searches);
+ }
- 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;
- }
+ // 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);
- // 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);
- }
- }
+ // Now add any favicons.
+ try {
+ insertFaviconsForTree(node);
+ } catch (ex) {
+ Cu.reportError(`Failed to insert favicons: ${ex}`);
+ }
+ }
- // 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);
- }
- 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;
+ // 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 });
+ }
}
},
-
- /**
- * 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");
}
/**
* Replaces imported folder ids with their local counterparts in a place: URI.
*
- * @param aURI
+ * @param {nsIURI} aQueryURI
* 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.
+ * @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.
+ */
+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;
+ }
+
+ 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 fixupQuery(aQueryURI, aFolderIdMap) {
- let convert = function(str, p1, offset, s) {
- return "folder=" + aFolderIdMap[p1];
+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;
+ }
}
- let stringURI = aQueryURI.spec.replace(/folder=([0-9]+)/g, convert);
+
+ // 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];
+}
- return NetUtil.newURI(stringURI);
+/**
+ * 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);
+ }
+ }
+}
--- a/toolkit/components/places/Bookmarks.jsm
+++ b/toolkit/components/places/Bookmarks.jsm
@@ -145,16 +145,22 @@ var Bookmarks = Object.freeze({
toolbarGuid: "toolbar_____",
unfiledGuid: "unfiled_____",
mobileGuid: "mobile______",
// With bug 424160, tags will stop being bookmarks, thus this root will
// 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.
*
* For creating a bookmark, the following set of properties is required:
* - type
* - parentGuid
* - url, only for bookmarked URLs
*
@@ -258,17 +264,17 @@ var Bookmarks = Object.freeze({
* ]
* }
*
* Children will be appended to any existing children of the parent
* that is specified. The source specified on the root of the tree
* will be used for all the items inserted. Any indices or custom parentGuids
* set on children will be ignored and overwritten.
*
- * @param tree
+ * @param {Object} tree
* object representing a tree of bookmark items to insert.
*
* @return {Promise} resolved when the creation is complete.
* @resolves to an object representing the created bookmark.
* @rejects if it's not possible to create the requested bookmark.
* @throws if the arguments are invalid.
*/
insertTree(tree) {
@@ -276,17 +282,17 @@ var Bookmarks = Object.freeze({
throw new Error("Should be provided a valid tree object.");
}
if (!Array.isArray(tree.children) || !tree.children.length) {
throw new Error("Should have a non-zero number of children to insert.");
}
if (!PlacesUtils.isValidGuid(tree.guid)) {
- throw new Error("The parent guid is not valid.");
+ throw new Error(`The parent guid is not valid (${tree.guid} ${tree.title}).`);
}
if (tree.guid == this.rootGuid) {
throw new Error("Can't insert into the root.");
}
if (tree.guid == this.tagsGuid) {
throw new Error("Can't use insertTree to insert tags.");
@@ -294,16 +300,17 @@ var Bookmarks = Object.freeze({
if (tree.hasOwnProperty("source") &&
!Object.values(this.SOURCES).includes(tree.source)) {
throw new Error("Can't use source value " + tree.source);
}
// 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
// we import (so they won't differ by a few ms based on where
// they are in the tree, and so we don't needlessly construct
// multiple dates).
let fallbackLastAdded = new Date();
@@ -351,26 +358,47 @@ var Bookmarks = Object.freeze({
dateAdded: { defaultValue: time,
validIf: b => !b.lastModified ||
b.dateAdded <= b.lastModified },
lastModified: { defaultValue: time,
validIf: b => (!b.dateAdded && b.lastModified >= time) ||
(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;
}
// Store the URL if this is a bookmark, so we can ensure we create an
// entry in moz_places for it.
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
// the children ref, as the default bookmark validators object doesn't
// know about children.
if (info.children) {
// start children of this item off at index 0.
let childrenLastAdded = appendInsertionInfoForInfoArray(info.children, 0, insertInfo.guid);
@@ -384,16 +412,17 @@ var Bookmarks = Object.freeze({
// Ensure we track what time to update the parent to.
if (insertInfo.dateAdded > lastAddedForParent) {
lastAddedForParent = insertInfo.dateAdded;
}
}
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.
let lastAddedForParent = appendInsertionInfoForInfoArray(tree.children, null, tree.guid);
return (async function() {
let parent = await fetchBookmark({ guid: tree.guid });
if (!parent) {
@@ -401,16 +430,19 @@ var Bookmarks = Object.freeze({
}
if (parent._parentId == PlacesUtils.tagsFolderId) {
throw new Error("Can't use insertTree to insert tags.");
}
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
// inserts will have been correct, and we don't want to query the DB
// again if we don't have to. bug 1347230 covers improving this.
let rootIndex = parent._childCount;
for (let insertInfo of insertInfos) {
if (insertInfo.parentGuid == tree.guid) {
@@ -421,23 +453,42 @@ var Bookmarks = Object.freeze({
// complete we may stop using them.
let itemIdMap = await PlacesUtils.promiseManyItemIds(insertInfos.map(info => info.guid));
// Notify onItemAdded to listeners.
let observers = PlacesUtils.bookmarks.getObservers();
for (let i = 0; i < insertInfos.length; i++) {
let item = insertInfos[i];
let itemId = itemIdMap.get(item.guid);
let uri = item.hasOwnProperty("url") ? PlacesUtils.toURI(item.url) : null;
- notify(observers, "onItemAdded", [ itemId, parent._id, item.index,
+ // 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,
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;
})();
},
/**
* Updates a bookmark-item.
@@ -693,28 +744,26 @@ var Bookmarks = Object.freeze({
* @return {Promise} resolved when the removal is complete.
* @resolves once the removal is complete.
*/
eraseEverything(options = {}) {
if (!options.source) {
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, folderGuids, options);
+ urls = await removeFoldersContents(db, Bookmarks.userContentRoots, options);
const time = PlacesUtils.toPRTime(new Date());
const syncChangeDelta =
PlacesSyncUtils.bookmarks.determineSyncChangeDelta(options.source);
- for (let folderGuid of folderGuids) {
+ for (let folderGuid of Bookmarks.userContentRoots) {
await db.executeCached(
`UPDATE moz_bookmarks SET lastModified = :time,
syncChangeCounter = syncChangeCounter + :syncChangeDelta
WHERE id IN (SELECT id FROM moz_bookmarks WHERE guid = :folderGuid )
`, { folderGuid, time, syncChangeDelta });
}
});
@@ -1312,16 +1361,26 @@ function insertBookmark(item, parent) {
// Don't return an empty title to the caller.
if (item.hasOwnProperty("title") && item.title === null)
delete item.title;
return item;
});
}
+/**
+ * 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() {
await maybeInsertManyPlaces(db, urls);
let syncChangeDelta =
PlacesSyncUtils.bookmarks.determineSyncChangeDelta(source);
let syncStatus =
@@ -1352,16 +1411,112 @@ function insertBookmarkTree(items, sourc
// We don't wait for the frecency calculation.
updateFrecency(db, urls, true).catch(Cu.reportError);
return items;
});
}
+/**
+ * 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) {
let queryParams = {
tags_folder: await promiseTagsFolderId(),
type: Bookmarks.TYPE_SEPARATOR,
};
// We're searching for bookmarks, so exclude tags and separators.
--- a/toolkit/components/places/PlacesBackups.jsm
+++ b/toolkit/components/places/PlacesBackups.jsm
@@ -76,16 +76,40 @@ function getBackupFileForSameDate(aFilen
for (let backupFile of backupFiles) {
if (isFilenameWithSameDate(OS.Path.basename(backupFile), aFilename))
return backupFile;
}
return null;
})();
}
+/**
+ * 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:
* 0: file name
* 1: date in form Y-m-d
* 2: bookmarks count
* 3: contents hash
* 4: file extension
@@ -539,11 +563,49 @@ this.PlacesBackups = {
try {
Services.telemetry
.getHistogramById("PLACES_BACKUPS_BOOKMARKSTREE_MS")
.add(Date.now() - startTime);
} catch (ex) {
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);
+ },
}
-
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -244,16 +244,21 @@ const BOOKMARK_VALIDATORS = Object.freez
if (typeof(v) === "string")
return new URL(v);
if (v instanceof Ci.nsIURI)
return new URL(v.spec);
return v;
},
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.
const SYNC_BOOKMARK_VALIDATORS = Object.freeze({
// Sync uses Places GUIDs for all records except roots.
syncId: simpleValidateFunc(v => typeof v == "string" && (
(PlacesSyncUtils.bookmarks.ROOTS.includes(v) ||
PlacesUtils.isValidGuid(v)))),
@@ -1569,18 +1574,18 @@ this.PlacesUtils = {
if (index != -1) {
this._bookmarksServiceObserversQueue.splice(index, 1);
}
},
/**
* Sets the character-set for a URI.
*
- * @param aURI nsIURI
- * @param aCharset character-set value.
+ * @param {nsIURI} aURI
+ * @param {String} aCharset character-set value.
* @return {Promise}
*/
setCharsetForURI: function PU_setCharsetForURI(aURI, aCharset) {
return new Promise(resolve => {
// Delaying to catch issues with asynchronous behavior while waiting
// to implement asynchronous annotations in bug 699844.
Services.tm.dispatchToMainThread(function() {
@@ -1718,23 +1723,32 @@ this.PlacesUtils = {
/**
* Get the item id for an item (a bookmark, a folder or a separator) given
* its unique id.
*
* @param aGuid
* an item GUID
* @return {Promise}
- * @resolves to the GUID.
+ * @resolves to the item id.
* @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);
},
/**
* Invalidate the GUID cache for the given itemId.
*
* @param aItemId
--- a/toolkit/components/places/tests/bookmarks/test_405938_restore_queries.js
+++ b/toolkit/components/places/tests/bookmarks/test_405938_restore_queries.js
@@ -35,90 +35,146 @@ scenarios:
*/
const DEFAULT_INDEX = PlacesUtils.bookmarks.DEFAULT_INDEX;
var test = {
_testRootId: null,
_testRootTitle: "test root",
- _folderIds: [],
+ _folderGuids: [],
_bookmarkURIs: [],
_count: 3,
+ _extraBookmarksCount: 10,
- populate: function populate() {
+ populate: async function populate() {
// folder to hold this test
- PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.toolbarFolderId);
- this._testRootId =
- PlacesUtils.bookmarks.createFolder(PlacesUtils.toolbarFolderId,
- this._testRootTitle, DEFAULT_INDEX);
+ 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);
// create test folders each with a bookmark
- 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);
+ 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}`,
+ }]
+ });
}
+ 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)
- this._queryURI1 = uri("place:folder=" + this._folderIds[0] + "&queryType=1");
+ 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._queryTitle1 = "query1";
- PlacesUtils.bookmarks.insertBookmark(this._testRootId, this._queryURI1,
- DEFAULT_INDEX, this._queryTitle1);
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: insertedBookmarks[0].guid,
+ dateAdded,
+ url: this._queryURI1,
+ title: this._queryTitle1
+ });
// create a query URI with _count folders
- this._queryURI2 = uri("place:folder=" + this._folderIds.join("&folder=") + "&queryType=1");
+ this._queryURI2 = `place:folder=${folderIds.join("&folder=")}&queryType=1`;
this._queryTitle2 = "query2";
- PlacesUtils.bookmarks.insertBookmark(this._testRootId, this._queryURI2,
- DEFAULT_INDEX, this._queryTitle2);
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: insertedBookmarks[0].guid,
+ dateAdded,
+ url: this._queryURI2,
+ title: this._queryTitle2
+ });
// create a query URI with _count queries (each with a folder)
// first get a query object for each folder
- var queries = this._folderIds.map(function(aFolderId) {
+ var queries = 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 =
- uri(PlacesUtils.history.queriesToQueryString(queries, queries.length, options));
+ PlacesUtils.history.queriesToQueryString(queries, queries.length, options);
this._queryTitle3 = "query3";
- PlacesUtils.bookmarks.insertBookmark(this._testRootId, this._queryURI3,
- DEFAULT_INDEX, this._queryTitle3);
+ 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");
},
clean() {},
- 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, "");
+ 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)),
+ });
+ }
}
var toolbar =
PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId,
false, true).root;
do_check_true(toolbar.childCount, 1);
var folderNode = toolbar.getChild(0);
do_check_eq(folderNode.type, folderNode.RESULT_TYPE_FOLDER);
do_check_eq(folderNode.title, this._testRootTitle);
folderNode.QueryInterface(Ci.nsINavHistoryQueryResultNode);
folderNode.containerOpen = true;
- // |_count| folders + the query node
- do_check_eq(folderNode.childCount, this._count + 3);
+ // |_count| folders + the query nodes
+ do_check_eq(folderNode.childCount, this._count + 4);
for (let i = 0; i < this._count; i++) {
var subFolder = folderNode.getChild(i);
do_check_eq(subFolder.title, "folder" + i);
subFolder.QueryInterface(Ci.nsINavHistoryContainerResultNode);
subFolder.containerOpen = true;
do_check_eq(subFolder.childCount, 1);
var child = subFolder.getChild(0);
@@ -130,31 +186,34 @@ var test = {
this.validateQueryNode1(folderNode.getChild(this._count));
// validate folders query
this.validateQueryNode2(folderNode.getChild(this._count + 1));
// 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;
},
validateQueryNode1: function validateQueryNode1(aNode) {
do_check_eq(aNode.title, this._queryTitle1);
do_check_true(PlacesUtils.nodeIsFolder(aNode));
aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
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;
},
validateQueryNode2: function validateQueryNode2(aNode) {
do_check_eq(aNode.title, this._queryTitle2);
do_check_true(PlacesUtils.nodeIsQuery(aNode));
aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
@@ -176,46 +235,57 @@ var test = {
aNode.containerOpen = true;
do_check_eq(aNode.childCount, this._count);
for (var i = 0; i < aNode.childCount; i++) {
var child = aNode.getChild(i);
do_check_true(uri(child.uri).equals(uri("http://" + i)))
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
- tests.forEach(function(aTest) {
- aTest.populate();
+ for (let singleTest of tests) {
+ await singleTest.populate();
// sanity
- aTest.validate();
- });
+ await singleTest.validate(true);
+ }
// export json to file
await BookmarkJSONUtils.exportToFile(jsonFile);
// clean
- tests.forEach(function(aTest) {
- aTest.clean();
- });
+ for (let singleTest of tests) {
+ singleTest.clean();
+ }
// restore json file
await BookmarkJSONUtils.importFromFile(jsonFile, true);
// validate
- tests.forEach(function(aTest) {
- aTest.validate();
- });
+ for (let singleTest of tests) {
+ await singleTest.validate(false);
+ }
// clean up
await OS.File.remove(jsonFile);
});
--- a/toolkit/components/places/tests/bookmarks/test_417228-exclude-from-backup.js
+++ b/toolkit/components/places/tests/bookmarks/test_417228-exclude-from-backup.js
@@ -1,15 +1,14 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et: */
/* 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/. */
-const EXCLUDE_FROM_BACKUP_ANNO = "places/excludeFromBackup";
// Menu, Toolbar, Unsorted, Tags, Mobile
const PLACES_ROOTS_COUNT = 5;
var tests = [];
/*
Backup/restore tests example:
@@ -44,27 +43,27 @@ var test = {
// add a test bookmark to be exclude
this._restoreRootExcludeURI = uri("http://exclude.uri");
var exItemId = PlacesUtils.bookmarks
.insertBookmark(restoreRootId,
this._restoreRootExcludeURI,
idx, "exclude uri");
// Annotate the bookmark for exclusion.
PlacesUtils.annotations.setItemAnnotation(exItemId,
- EXCLUDE_FROM_BACKUP_ANNO, 1, 0,
+ PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1, 0,
PlacesUtils.annotations.EXPIRE_NEVER);
// create a root to be exclude
this._excludeRootTitle = "exclude root";
this._excludeRootId = PlacesUtils.bookmarks
.createFolder(PlacesUtils.placesRootId,
this._excludeRootTitle, idx);
// Annotate the root for exclusion.
PlacesUtils.annotations.setItemAnnotation(this._excludeRootId,
- EXCLUDE_FROM_BACKUP_ANNO, 1, 0,
+ PlacesUtils.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,
this._restoreRootExcludeURI,
idx, "exclude uri");
},
validate: function validate(aEmptyBookmarks) {
@@ -100,36 +99,40 @@ var test = {
var restoreRootChildNode = restoreRootNode.getChild(0);
do_check_eq(restoreRootChildNode.uri, this._restoreRootURI.spec);
restoreRootNode.containerOpen = false;
rootNode.containerOpen = false;
}
}
-function run_test() {
- run_next_test();
-}
+// make json file
+var jsonFile;
-add_task(async function() {
- // make json file
- let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json");
+add_task(async function setup() {
+ 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
PlacesUtils.bookmarks.removeItem(test._excludeRootId);
// restore json file
await BookmarkJSONUtils.importFromFile(jsonFile, true);
// validate after a complete bookmarks cleanup
--- a/toolkit/components/places/tests/bookmarks/test_417228-other-roots.js
+++ b/toolkit/components/places/tests/bookmarks/test_417228-other-roots.js
@@ -15,16 +15,19 @@ var myTest = {
validate: function () { ... query for your bookmarks ... }
}
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,
false, false).root;
do_check_eq(rootNode.childCount, 5);
// create a test root
this._folderTitle = "test folder";
@@ -114,20 +117,16 @@ tests.push({
node.containerOpen = false;
}
}
do_check_eq(foundTestFolder, 1);
rootNode.containerOpen = false;
}
});
-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
tests.forEach(function(aTest) {
aTest.populate();
// sanity
--- a/toolkit/components/places/tests/unit/bookmarks.json
+++ b/toolkit/components/places/tests/unit/bookmarks.json
@@ -45,38 +45,58 @@
"parent": 6,
"dateAdded": 1361551979365662,
"lastModified": 1361551979368077,
"type": "text/x-moz-place",
"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,
"title": "Get Involved",
"id": 9,
"parent": 6,
"dateAdded": 1361551979371071,
"lastModified": 1361551979373745,
"type": "text/x-moz-place",
"uri": "http://en-us.www.mozilla.com/en-US/firefox/community/",
"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,
+ "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"
+ }
+ ],
"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=="
+ "uri": "http://example.com/search?q=%s&suggid=",
+ "postData": null
}
]
},
{
"guid": "OCyeUO5uu9FK",
"index": 1,
"title": "",
"id": 11,
--- a/toolkit/components/places/tests/unit/test_384370.js
+++ b/toolkit/components/places/tests/unit/test_384370.js
@@ -6,20 +6,16 @@ var tagData = [
{ uri: uri("http://en.wikipedia.org/wiki/Diplodocus"), tags: ["dinosaur", "dj", "rad word"] }
];
var bookmarkData = [
{ uri: uri("http://slint.us"), title: "indie, kentucky, music" },
{ 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
- export as json, import, test (tests integrity of html > json)
- export as html, import, test (tests integrity of json > html)
BACKUP/RESTORE SUMMARY:
- create a bookmark in each root
@@ -47,37 +43,41 @@ add_task(async function() {
title });
}
for (let { uri, title } of bookmarkData) {
await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
url: uri,
title });
}
- await validate();
+ await validate("initial database");
// Test exporting a Places canonical json file.
// 1. export to bookmarks.exported.json
await BookmarkJSONUtils.exportToFile(jsonFile);
do_print("exported json");
// 2. empty bookmarks db
// 3. import bookmarks.exported.json
await BookmarkJSONUtils.importFromFile(jsonFile, true);
do_print("imported json");
// 4. run the test-suite
- await validate();
+ await validate("re-imported json");
do_print("validated import");
});
-async function validate() {
+async function validate(infoMsg) {
+ do_print(`Validating ${infoMsg}: testMenuBookmarks`);
await testMenuBookmarks();
+ do_print(`Validating ${infoMsg}: testToolbarBookmarks`);
await testToolbarBookmarks();
+ do_print(`Validating ${infoMsg}: testUnfiledBookmarks`);
testUnfiledBookmarks();
+ do_print(`Validating ${infoMsg}: testTags`);
testTags();
await PlacesTestUtils.promiseAsyncUpdates();
}
// Tests a bookmarks datastore that has a set of bookmarks, etc
// that flex each supported field and feature.
async function testMenuBookmarks() {
let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarksMenuFolderId).root;
--- a/toolkit/components/places/tests/unit/test_bookmarks_json.js
+++ b/toolkit/components/places/tests/unit/test_bookmarks_json.js
@@ -31,16 +31,20 @@ var test_bookmarks = {
title: "Get Involved",
url: "http://en-us.www.mozilla.com/en-US/firefox/community/",
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",
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="
}
]
},
{
guid: "OCyeUO5uu9FK",
type: Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR
},
{
@@ -67,17 +71,20 @@ var test_bookmarks = {
{ guid: "OCyeUO5uu9FB",
title: "Getting Started",
url: "http://en-us.www.mozilla.com/en-US/firefox/central/",
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: "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"
+ 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,
}
],
unfiled: [
{ guid: "OCyeUO5uu9FW",
title: "Example.tld",
url: "http://example.tld/"
}
]
@@ -187,17 +194,17 @@ function checkItem(aExpected, aNode) {
PlacesUtils.favicons.getFaviconDataForPage(
NetUtil.newURI(aExpected.url),
function(aURI, aDataLen, aData, aMimeType) {
deferred.resolve(aData);
});
let data = await deferred.promise;
let base64Icon = "data:image/png;base64," +
base64EncodeString(String.fromCharCode.apply(String, data));
- do_check_true(base64Icon == aExpected.icon);
+ do_check_eq(base64Icon, aExpected.icon);
break;
case "keyword": {
let entry = await PlacesUtils.keywords.fetch({ url: aNode.uri });
Assert.equal(entry.keyword, aExpected.keyword);
break;
}
case "guid":
let guid = await PlacesUtils.promiseItemGuid(id);
--- a/toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js
+++ b/toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js
@@ -72,18 +72,18 @@ add_task(async function test_import_mobi
await treeEquals(PlacesUtils.bookmarks.rootGuid, {
guid: PlacesUtils.bookmarks.rootGuid,
index: 0,
children: [{
guid: PlacesUtils.bookmarks.menuGuid,
index: 0,
children: [
- { guid: "Utodo9b0oVws", index: 0 },
- { guid: "X6lUyOspVYwi", index: 1 },
+ { guid: "X6lUyOspVYwi", index: 0 },
+ { guid: "Utodo9b0oVws", index: 1 },
],
}, {
guid: PlacesUtils.bookmarks.toolbarGuid,
index: 1,
}, {
guid: PlacesUtils.bookmarks.unfiledGuid,
index: 3,
}, {
@@ -91,20 +91,22 @@ add_task(async function test_import_mobi
index: 4,
annos: [{
name: "mobile/bookmarksRoot",
flags: 0,
expires: 4,
value: 1,
}],
children: [
- { guid: "a17yW6-nTxEJ", index: 0 },
- { guid: "xV10h9Wi3FBM", index: 1 },
- { guid: "_o8e1_zxTJFg", index: 2 },
- { guid: "QCtSqkVYUbXB", index: 3 },
+ // 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 },
],
}],
}, "Should merge bookmarks root contents");
await PlacesUtils.bookmarks.eraseEverything();
});
add_task(async function test_restore_mobile_bookmarks_folder() {
@@ -164,19 +166,19 @@ add_task(async function test_import_mobi
await treeEquals(PlacesUtils.bookmarks.rootGuid, {
guid: PlacesUtils.bookmarks.rootGuid,
index: 0,
children: [{
guid: PlacesUtils.bookmarks.menuGuid,
index: 0,
children: [
- { guid: "Utodo9b0oVws", index: 0 },
- { guid: "X6lUyOspVYwi", index: 1 },
- { guid: "XF4yRP6bTuil", index: 2 },
+ { guid: "X6lUyOspVYwi", index: 0 },
+ { guid: "XF4yRP6bTuil", index: 1 },
+ { guid: "Utodo9b0oVws", index: 2 },
],
}, {
guid: PlacesUtils.bookmarks.toolbarGuid,
index: 1,
children: [{ guid: "buy7711R3ZgE", index: 0 }],
}, {
guid: PlacesUtils.bookmarks.unfiledGuid,
index: 3,
@@ -186,20 +188,20 @@ add_task(async function test_import_mobi
index: 4,
annos: [{
name: "mobile/bookmarksRoot",
flags: 0,
expires: 4,
value: 1,
}],
children: [
- { guid: "a17yW6-nTxEJ", index: 0 },
- { guid: "xV10h9Wi3FBM", index: 1 },
- { guid: "_o8e1_zxTJFg", index: 2 },
- { guid: "QCtSqkVYUbXB", index: 3 },
+ { guid: "_o8e1_zxTJFg", index: 0 },
+ { guid: "QCtSqkVYUbXB", index: 1 },
+ { guid: "a17yW6-nTxEJ", index: 2 },
+ { guid: "xV10h9Wi3FBM", index: 3 },
],
}],
}, "Should merge bookmarks folder contents into mobile root");
await PlacesUtils.bookmarks.eraseEverything();
});
add_task(async function test_restore_multiple_bookmarks_folders() {
@@ -230,18 +232,18 @@ add_task(async function test_restore_mul
index: 4,
annos: [{
name: "mobile/bookmarksRoot",
flags: 0,
expires: 4,
value: 1,
}],
children: [
- { guid: "sSZ86WT9WbN3", index: 0 },
- { guid: "a17yW6-nTxEJ", index: 1 },
+ { guid: "a17yW6-nTxEJ", index: 0 },
+ { guid: "sSZ86WT9WbN3", index: 1 },
],
}],
}, "Should restore multiple bookmarks folder contents into root");
await PlacesUtils.bookmarks.eraseEverything();
});
add_task(async function test_import_multiple_bookmarks_folders() {
@@ -252,20 +254,20 @@ add_task(async function test_import_mult
await treeEquals(PlacesUtils.bookmarks.rootGuid, {
guid: PlacesUtils.bookmarks.rootGuid,
index: 0,
children: [{
guid: PlacesUtils.bookmarks.menuGuid,
index: 0,
children: [
- { guid: "buy7711R3ZgE", index: 0 },
- { guid: "F_LBgd1fS_uQ", index: 1 },
- { guid: "oIpmQXMWsXvY", index: 2 },
- { guid: "X6lUyOspVYwi", index: 3 },
+ { guid: "X6lUyOspVYwi", index: 0 },
+ { guid: "buy7711R3ZgE", index: 1 },
+ { guid: "F_LBgd1fS_uQ", index: 2 },
+ { guid: "oIpmQXMWsXvY", index: 3 },
],
}, {
guid: PlacesUtils.bookmarks.toolbarGuid,
index: 1,
children: [{ guid: "Utodo9b0oVws", index: 0 }],
}, {
guid: PlacesUtils.bookmarks.unfiledGuid,
index: 3,
@@ -275,18 +277,18 @@ add_task(async function test_import_mult
index: 4,
annos: [{
name: "mobile/bookmarksRoot",
flags: 0,
expires: 4,
value: 1,
}],
children: [
- { guid: "sSZ86WT9WbN3", index: 0 },
- { guid: "a17yW6-nTxEJ", index: 1 },
- { guid: "_o8e1_zxTJFg", index: 2 },
- { guid: "QCtSqkVYUbXB", index: 3 },
+ { guid: "_o8e1_zxTJFg", index: 0 },
+ { guid: "QCtSqkVYUbXB", index: 1 },
+ { guid: "a17yW6-nTxEJ", index: 2 },
+ { guid: "sSZ86WT9WbN3", index: 3 },
],
}],
}, "Should merge multiple mobile folders into root");
await PlacesUtils.bookmarks.eraseEverything();
});
--- a/toolkit/components/places/tests/unit/test_sync_utils.js
+++ b/toolkit/components/places/tests/unit/test_sync_utils.js
@@ -1,27 +1,23 @@
Cu.import("resource://gre/modules/ObjectUtils.jsm");
Cu.import("resource://gre/modules/PlacesSyncUtils.jsm");
const {
// `fetchGuidsWithAnno` isn't exported, but we can still access it here via a
// backstage pass.
fetchGuidsWithAnno,
} = Cu.import("resource://gre/modules/PlacesSyncUtils.jsm", {});
Cu.import("resource://testing-common/httpd.js");
-Cu.importGlobalProperties(["crypto", "URLSearchParams"]);
+Cu.importGlobalProperties(["URLSearchParams"]);
const DESCRIPTION_ANNO = "bookmarkProperties/description";
const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
const SYNC_PARENT_ANNO = "sync/parent";
-function makeGuid() {
- return ChromeUtils.base64URLEncode(crypto.getRandomValues(new Uint8Array(9)), {
- pad: false,
- });
-}
+var makeGuid = PlacesUtils.history.makeGuid;
function makeLivemarkServer() {
let server = new HttpServer();
server.registerPrefixHandler("/feed/", do_get_file("./livemark.xml"));
server.start(-1);
return {
server,
get site() {