--- a/toolkit/components/places/BookmarkJSONUtils.jsm
+++ b/toolkit/components/places/BookmarkJSONUtils.jsm
@@ -317,43 +317,51 @@ BookmarkImporter.prototype = {
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 {
- this.importJSONNode(
+ 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
- searchIds.forEach(function(aId) {
- let oldURI = PlacesUtils.bookmarks.getBookmarkURI(aId);
+ for (let id of searchIds) {
+ let oldURI = PlacesUtils.bookmarks.getBookmarkURI(id);
let uri = fixupQuery(oldURI, folderIdMap);
if (!uri.equals(oldURI)) {
- PlacesUtils.bookmarks.changeBookmarkURI(aId, uri, this._source);
+ PlacesUtils.bookmarks.changeBookmarkURI(id, uri, this._source);
}
- });
+ }
deferred.resolve();
}.bind(this)
};
PlacesUtils.bookmarks.runInBatchMode(batch, null);
}
yield deferred.promise;
@@ -434,18 +442,30 @@ BookmarkImporter.prototype = {
this._source);
if (aData.annos && aData.annos.length)
PlacesUtils.setAnnotationsForItem(id, aData.annos,
this._source);
});
this._importPromises.push(lmPromise);
}
} else {
- id = PlacesUtils.bookmarks.createFolder(
- aContainer, aData.title, aIndex, aData.guid, this._source);
+ 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++) {
@@ -520,18 +540,20 @@ BookmarkImporter.prototype = {
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
- if (id != -1 && aContainer != PlacesUtils.tagsFolderId &&
+ // 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)
--- a/toolkit/components/places/Bookmarks.jsm
+++ b/toolkit/components/places/Bookmarks.jsm
@@ -113,16 +113,17 @@ var Bookmarks = Object.freeze({
* Special GUIDs associated with bookmark roots.
* It's guaranteed that the roots will always have these guids.
*/
rootGuid: "root________",
menuGuid: "menu________",
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________",
/**
* Inserts a bookmark-item into the bookmarks tree.
*
@@ -405,17 +406,17 @@ var Bookmarks = Object.freeze({
let info = guidOrInfo;
if (!info)
throw new Error("Input should be a valid object");
if (typeof(guidOrInfo) != "object")
info = { guid: guidOrInfo };
// Disallow removing the root folders.
if ([this.rootGuid, this.menuGuid, this.toolbarGuid, this.unfiledGuid,
- this.tagsGuid].includes(info.guid)) {
+ this.tagsGuid, this.mobileGuid].includes(info.guid)) {
throw new Error("It's not possible to remove Places root folders.");
}
// Even if we ignore any other unneeded property, we still validate any
// known property to reduce likelihood of hidden bugs.
let removeInfo = validateBookmarkObject(info);
return Task.spawn(function* () {
@@ -460,17 +461,18 @@ var Bookmarks = Object.freeze({
* Defaults to nsINavBookmarksService::SOURCE_DEFAULT.
*
* @return {Promise} resolved when the removal is complete.
* @resolves once the removal is complete.
*/
eraseEverything: function(options={}) {
return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: eraseEverything",
db => db.executeTransaction(function* () {
- const folderGuids = [this.toolbarGuid, this.menuGuid, this.unfiledGuid];
+ const folderGuids = [this.toolbarGuid, this.menuGuid, this.unfiledGuid,
+ this.mobileGuid];
yield removeFoldersContents(db, folderGuids, options);
const time = PlacesUtils.toPRTime(new Date());
for (let folderGuid of folderGuids) {
yield db.executeCached(
`UPDATE moz_bookmarks SET lastModified = :time
WHERE id IN (SELECT id FROM moz_bookmarks WHERE guid = :folderGuid )
`, { folderGuid, time });
}
--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -3,16 +3,17 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/ArrayUtils.h"
#include "mozilla/Attributes.h"
#include "mozilla/DebugOnly.h"
#include "Database.h"
+#include "nsIAnnotationService.h"
#include "nsINavBookmarksService.h"
#include "nsIInterfaceRequestorUtils.h"
#include "nsIFile.h"
#include "nsIWritablePropertyBag2.h"
#include "nsNavHistory.h"
#include "nsPlacesTables.h"
#include "nsPlacesIndexes.h"
@@ -84,16 +85,25 @@
// Places string bundle, contains internationalized bookmark root names.
#define PLACES_BUNDLE "chrome://places/locale/places.properties"
// Livemarks annotations.
#define LMANNO_FEEDURI "livemark/feedURI"
#define LMANNO_SITEURI "livemark/siteURI"
+#define MOBILE_ROOT_GUID "mobile______"
+#define MOBILE_ROOT_ANNO "mobile/bookmarksRoot"
+#define MOBILE_QUERY_ANNO "MobileBookmarks"
+
+// We use a fixed title for the mobile root to avoid marking the database as
+// corrupt if we can't look up the localized title in the string bundle. Sync
+// sets the title to the localized version when it creates the left pane query.
+#define MOBILE_ROOT_TITLE "mobile"
+
using namespace mozilla;
namespace mozilla {
namespace places {
namespace {
////////////////////////////////////////////////////////////////////////////////
@@ -856,16 +866,23 @@ Database::InitSchema(bool* aDatabaseMigr
if (currentSchemaVersion < 34) {
rv = MigrateV34Up();
NS_ENSURE_SUCCESS(rv, rv);
}
// Firefox 51 uses schema version 34.
+ if (currentSchemaVersion < 35) {
+ rv = MigrateV35Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Firefox 52 uses schema version 35.
+
// Schema Upgrades must add migration code here.
rv = UpdateBookmarkRootTitles();
// We don't want a broken localization to cause us to think
// the database is corrupt and needs to be replaced.
MOZ_ASSERT(NS_SUCCEEDED(rv));
}
}
@@ -1010,30 +1027,33 @@ Database::CreateBookmarkRoots()
rv = bundle->GetStringFromName(u"OtherBookmarksFolderTitle",
getter_Copies(rootTitle));
if (NS_FAILED(rv)) return rv;
rv = CreateRoot(mMainConn, NS_LITERAL_CSTRING("unfiled"),
NS_LITERAL_CSTRING("unfiled_____"), rootTitle);
if (NS_FAILED(rv)) return rv;
+ int64_t mobileRootId = CreateMobileRoot();
+ if (mobileRootId <= 0) return NS_ERROR_FAILURE;
+
#if DEBUG
nsCOMPtr<mozIStorageStatement> stmt;
rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
"SELECT count(*), sum(position) FROM moz_bookmarks"
), getter_AddRefs(stmt));
if (NS_FAILED(rv)) return rv;
bool hasResult;
rv = stmt->ExecuteStep(&hasResult);
if (NS_FAILED(rv)) return rv;
MOZ_ASSERT(hasResult);
int32_t bookmarkCount = stmt->AsInt32(0);
int32_t positionSum = stmt->AsInt32(1);
- MOZ_ASSERT(bookmarkCount == 5 && positionSum == 6);
+ MOZ_ASSERT(bookmarkCount == 6 && positionSum == 10);
#endif
return NS_OK;
}
nsresult
Database::InitFunctions()
{
@@ -1122,21 +1142,23 @@ Database::UpdateBookmarkRootTitles()
nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
rv = stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
if (NS_FAILED(rv)) return rv;
const char *rootGuids[] = { "menu________"
, "toolbar_____"
, "tags________"
, "unfiled_____"
+ , "mobile______"
};
const char *titleStringIDs[] = { "BookmarksMenuFolderTitle"
, "BookmarksToolbarFolderTitle"
, "TagsFolderTitle"
, "OtherBookmarksFolderTitle"
+ , "MobileBookmarksFolderTitle"
};
for (uint32_t i = 0; i < ArrayLength(rootGuids); ++i) {
nsXPIDLString title;
rv = bundle->GetStringFromName(NS_ConvertASCIItoUTF16(titleStringIDs[i]).get(),
getter_Copies(title));
if (NS_FAILED(rv)) return rv;
@@ -1830,16 +1852,261 @@ Database::MigrateV34Up() {
"WHERE NOT EXISTS (SELECT 1 FROM moz_places h WHERE k.place_id = h.id) "
")"
));
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
+nsresult
+Database::MigrateV35Up() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ int64_t mobileRootId = CreateMobileRoot();
+ if (mobileRootId <= 0) return NS_ERROR_FAILURE;
+
+ // At this point, we should have no more than two folders with the mobile
+ // bookmarks anno: the new root, and the old folder if one exists. If, for
+ // some reason, we have multiple folders with the anno, we append their
+ // children to the new root.
+ nsTArray<int64_t> folderIds;
+ nsresult rv = GetItemsWithAnno(NS_LITERAL_CSTRING(MOBILE_ROOT_ANNO),
+ nsINavBookmarksService::TYPE_FOLDER,
+ folderIds);
+ if (NS_FAILED(rv)) return rv;
+
+ for (uint32_t i = 0; i < folderIds.Length(); ++i) {
+ if (folderIds[i] == mobileRootId) {
+ // Ignore the new mobile root. We'll remove this anno from the root in
+ // bug 1306445.
+ continue;
+ }
+
+ // Append the folder's children to the new root.
+ nsCOMPtr<mozIStorageStatement> moveStmt;
+ rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "UPDATE moz_bookmarks "
+ "SET parent = :root_id, "
+ "position = position + IFNULL("
+ "(SELECT MAX(position) + 1 FROM moz_bookmarks "
+ "WHERE parent = :root_id), 0)"
+ "WHERE parent = :folder_id"
+ ), getter_AddRefs(moveStmt));
+ if (NS_FAILED(rv)) return rv;
+ mozStorageStatementScoper moveScoper(moveStmt);
+
+ rv = moveStmt->BindInt64ByName(NS_LITERAL_CSTRING("root_id"),
+ mobileRootId);
+ if (NS_FAILED(rv)) return rv;
+ rv = moveStmt->BindInt64ByName(NS_LITERAL_CSTRING("folder_id"),
+ folderIds[i]);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = moveStmt->Execute();
+ if (NS_FAILED(rv)) return rv;
+
+ // Delete the old folder.
+ rv = DeleteBookmarkItem(folderIds[i]);
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ // Delete any left pane queries that point to the old folder. We'll
+ // automatically create one for the new mobile root during the next sync.
+ // Other queries like `place:folder={folderId}` might become invalid; we
+ // ignore them because they're unlikely to exist, and rewriting query URLs
+ // is tedious.
+ nsTArray<int64_t> queryIds;
+ rv = GetItemsWithAnno(NS_LITERAL_CSTRING(MOBILE_QUERY_ANNO),
+ nsINavBookmarksService::TYPE_BOOKMARK,
+ queryIds);
+ if (NS_FAILED(rv)) return rv;
+
+ for (uint32_t i = 0; i < queryIds.Length(); ++i) {
+ rv = DeleteBookmarkItem(queryIds[i]);
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+Database::GetItemsWithAnno(const nsACString& aAnnoName, int32_t aItemType,
+ nsTArray<int64_t>& aItemIds)
+{
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT b.id FROM moz_items_annos a "
+ "JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id "
+ "JOIN moz_bookmarks b ON b.id = a.item_id "
+ "WHERE n.name = :anno_name AND "
+ "b.type = :item_type"
+ ), getter_AddRefs(stmt));
+ if (NS_FAILED(rv)) return rv;
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("anno_name"), aAnnoName);
+ if (NS_FAILED(rv)) return rv;
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_type"), aItemType);
+ if (NS_FAILED(rv)) return rv;
+
+ bool hasMore = false;
+ while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
+ int64_t itemId;
+ rv = stmt->GetInt64(0, &itemId);
+ if (NS_FAILED(rv)) return rv;
+ aItemIds.AppendElement(itemId);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+Database::DeleteBookmarkItem(int32_t aItemId)
+{
+ // Delete the old bookmark.
+ nsCOMPtr<mozIStorageStatement> deleteStmt;
+ nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_bookmarks WHERE id = :item_id"
+ ), getter_AddRefs(deleteStmt));
+ if (NS_FAILED(rv)) return rv;
+ mozStorageStatementScoper deleteScoper(deleteStmt);
+
+ rv = deleteStmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"),
+ aItemId);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = deleteStmt->Execute();
+ if (NS_FAILED(rv)) return rv;
+
+ // Clean up orphan annotations.
+ nsCOMPtr<mozIStorageStatement> removeAnnosStmt;
+ rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_items_annos WHERE item_id = :item_id"
+ ), getter_AddRefs(removeAnnosStmt));
+ if (NS_FAILED(rv)) return rv;
+ mozStorageStatementScoper removeAnnosScoper(removeAnnosStmt);
+
+ rv = removeAnnosStmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"),
+ aItemId);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = removeAnnosStmt->Execute();
+ if (NS_FAILED(rv)) return rv;
+
+ return NS_OK;
+}
+
+int64_t
+Database::CreateMobileRoot()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Create the mobile root, ignoring conflicts if one already exists (for
+ // example, if the user downgraded to an earlier release channel).
+ nsCOMPtr<mozIStorageStatement> createStmt;
+ nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "INSERT OR IGNORE INTO moz_bookmarks "
+ "(type, title, dateAdded, lastModified, guid, position, parent) "
+ "SELECT :item_type, :item_title, :timestamp, :timestamp, :guid, "
+ "(SELECT COUNT(*) FROM moz_bookmarks p WHERE p.parent = b.id), b.id "
+ "FROM moz_bookmarks b WHERE b.parent = 0"
+ ), getter_AddRefs(createStmt));
+ if (NS_FAILED(rv)) return -1;
+ mozStorageStatementScoper createScoper(createStmt);
+
+ rv = createStmt->BindInt32ByName(NS_LITERAL_CSTRING("item_type"),
+ nsINavBookmarksService::TYPE_FOLDER);
+ if (NS_FAILED(rv)) return -1;
+ rv = createStmt->BindUTF8StringByName(NS_LITERAL_CSTRING("item_title"),
+ NS_LITERAL_CSTRING(MOBILE_ROOT_TITLE));
+ if (NS_FAILED(rv)) return -1;
+ rv = createStmt->BindInt64ByName(NS_LITERAL_CSTRING("timestamp"),
+ RoundedPRNow());
+ if (NS_FAILED(rv)) return -1;
+ rv = createStmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"),
+ NS_LITERAL_CSTRING(MOBILE_ROOT_GUID));
+ if (NS_FAILED(rv)) return -1;
+
+ rv = createStmt->Execute();
+ if (NS_FAILED(rv)) return -1;
+
+ // Find the mobile root ID. We can't use the last inserted ID because the
+ // root might already exist, and we ignore on conflict.
+ nsCOMPtr<mozIStorageStatement> findIdStmt;
+ rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT id FROM moz_bookmarks WHERE guid = :guid"
+ ), getter_AddRefs(findIdStmt));
+ if (NS_FAILED(rv)) return -1;
+ mozStorageStatementScoper findIdScoper(findIdStmt);
+
+ rv = findIdStmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"),
+ NS_LITERAL_CSTRING(MOBILE_ROOT_GUID));
+ if (NS_FAILED(rv)) return -1;
+
+ bool hasResult = false;
+ rv = findIdStmt->ExecuteStep(&hasResult);
+ if (NS_FAILED(rv) || !hasResult) return -1;
+
+ int64_t rootId;
+ rv = findIdStmt->GetInt64(0, &rootId);
+ if (NS_FAILED(rv)) return -1;
+
+ // Set the mobile bookmarks anno on the new root, so that Sync code on an
+ // older channel can still find it in case of a downgrade. This can be
+ // removed in bug 1306445.
+ nsCOMPtr<mozIStorageStatement> addAnnoNameStmt;
+ rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "INSERT OR IGNORE INTO moz_anno_attributes (name) VALUES (:anno_name)"
+ ), getter_AddRefs(addAnnoNameStmt));
+ if (NS_FAILED(rv)) return -1;
+ mozStorageStatementScoper addAnnoNameScoper(addAnnoNameStmt);
+
+ rv = addAnnoNameStmt->BindUTF8StringByName(
+ NS_LITERAL_CSTRING("anno_name"), NS_LITERAL_CSTRING(MOBILE_ROOT_ANNO));
+ if (NS_FAILED(rv)) return -1;
+ rv = addAnnoNameStmt->Execute();
+ if (NS_FAILED(rv)) return -1;
+
+ nsCOMPtr<mozIStorageStatement> addAnnoStmt;
+ rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "INSERT OR IGNORE INTO moz_items_annos "
+ "(id, item_id, anno_attribute_id, content, flags, "
+ "expiration, type, dateAdded, lastModified) "
+ "SELECT "
+ "(SELECT a.id FROM moz_items_annos a "
+ "WHERE a.anno_attribute_id = n.id AND "
+ "a.item_id = :root_id), "
+ ":root_id, n.id, 1, 0, :expiration, :type, :timestamp, :timestamp "
+ "FROM moz_anno_attributes n WHERE name = :anno_name"
+ ), getter_AddRefs(addAnnoStmt));
+ if (NS_FAILED(rv)) return -1;
+ mozStorageStatementScoper addAnnoScoper(addAnnoStmt);
+
+ rv = addAnnoStmt->BindInt64ByName(NS_LITERAL_CSTRING("root_id"), rootId);
+ if (NS_FAILED(rv)) return -1;
+ rv = addAnnoStmt->BindUTF8StringByName(
+ NS_LITERAL_CSTRING("anno_name"), NS_LITERAL_CSTRING(MOBILE_ROOT_ANNO));
+ if (NS_FAILED(rv)) return -1;
+ rv = addAnnoStmt->BindInt32ByName(NS_LITERAL_CSTRING("expiration"),
+ nsIAnnotationService::EXPIRE_NEVER);
+ if (NS_FAILED(rv)) return -1;
+ rv = addAnnoStmt->BindInt32ByName(NS_LITERAL_CSTRING("type"),
+ nsIAnnotationService::TYPE_INT32);
+ if (NS_FAILED(rv)) return -1;
+ rv = addAnnoStmt->BindInt32ByName(NS_LITERAL_CSTRING("timestamp"),
+ RoundedPRNow());
+ if (NS_FAILED(rv)) return -1;
+
+ rv = addAnnoStmt->Execute();
+ if (NS_FAILED(rv)) return -1;
+
+ return rootId;
+}
+
void
Database::Shutdown()
{
// As the last step in the shutdown path, finalize the database handle.
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(!mClosed);
// Break cycles with the shutdown blockers.
--- a/toolkit/components/places/Database.h
+++ b/toolkit/components/places/Database.h
@@ -13,17 +13,17 @@
#include "mozilla/storage.h"
#include "mozilla/storage/StatementCache.h"
#include "mozilla/Attributes.h"
#include "nsIEventTarget.h"
#include "Shutdown.h"
// This is the schema version. Update it at any schema change and add a
// corresponding migrateVxx method below.
-#define DATABASE_SCHEMA_VERSION 34
+#define DATABASE_SCHEMA_VERSION 35
// Fired after Places inited.
#define TOPIC_PLACES_INIT_COMPLETE "places-init-complete"
// Fired when initialization fails due to a locked database.
#define TOPIC_DATABASE_LOCKED "places-database-locked"
// This topic is received when the profile is about to be lost. Places does
// initial shutdown work and notifies TOPIC_PLACES_SHUTDOWN to all listeners.
// Any shutdown work that requires the Places APIs should happen here.
@@ -265,21 +265,27 @@ protected:
nsresult MigrateV26Up();
nsresult MigrateV27Up();
nsresult MigrateV28Up();
nsresult MigrateV30Up();
nsresult MigrateV31Up();
nsresult MigrateV32Up();
nsresult MigrateV33Up();
nsresult MigrateV34Up();
+ nsresult MigrateV35Up();
nsresult UpdateBookmarkRootTitles();
friend class ConnectionShutdownBlocker;
+ int64_t CreateMobileRoot();
+ nsresult GetItemsWithAnno(const nsACString& aAnnoName, int32_t aItemType,
+ nsTArray<int64_t>& aItemIds);
+ nsresult DeleteBookmarkItem(int32_t aItemId);
+
private:
~Database();
/**
* Singleton getter, invoked by class instantiation.
*/
static already_AddRefed<Database> GetSingleton();
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -303,16 +303,17 @@ this.PlacesUtils = {
TYPE_X_MOZ_PLACE_ACTION: "text/x-moz-place-action",
EXCLUDE_FROM_BACKUP_ANNO: "places/excludeFromBackup",
LMANNO_FEEDURI: "livemark/feedURI",
LMANNO_SITEURI: "livemark/siteURI",
POST_DATA_ANNO: "bookmarkProperties/POSTData",
READ_ONLY_ANNO: "placesInternal/READ_ONLY",
CHARSET_ANNO: "URIProperties/characterSet",
+ MOBILE_ROOT_ANNO: "mobile/bookmarksRoot",
TOPIC_SHUTDOWN: "places-shutdown",
TOPIC_INIT_COMPLETE: "places-init-complete",
TOPIC_DATABASE_LOCKED: "places-database-locked",
TOPIC_EXPIRATION_FINISHED: "places-expiration-finished",
TOPIC_FEEDBACK_UPDATED: "places-autocomplete-feedback-updated",
TOPIC_FAVICONS_EXPIRED: "places-favicons-expired",
TOPIC_VACUUM_STARTING: "places-vacuum-starting",
@@ -1105,29 +1106,35 @@ this.PlacesUtils = {
return this.tagsFolderId = this.bookmarks.tagsFolder;
},
get unfiledBookmarksFolderId() {
delete this.unfiledBookmarksFolderId;
return this.unfiledBookmarksFolderId = this.bookmarks.unfiledBookmarksFolder;
},
+ get mobileFolderId() {
+ delete this.mobileFolderId;
+ return this.mobileFolderId = this.bookmarks.mobileFolder;
+ },
+
/**
* Checks if aItemId is a root.
*
* @param aItemId
* item id to look for.
* @returns true if aItemId is a root, false otherwise.
*/
isRootItem: function PU_isRootItem(aItemId) {
return aItemId == PlacesUtils.bookmarksMenuFolderId ||
aItemId == PlacesUtils.toolbarFolderId ||
aItemId == PlacesUtils.unfiledBookmarksFolderId ||
aItemId == PlacesUtils.tagsFolderId ||
- aItemId == PlacesUtils.placesRootId;
+ aItemId == PlacesUtils.placesRootId ||
+ aItemId == PlacesUtils.mobileFolderId;
},
/**
* Set the POST data associated with a bookmark, if any.
* Used by POST keywords.
* @param aBookmarkId
*
* @deprecated Use PlacesUtils.keywords.insert() API instead.
@@ -1810,16 +1817,18 @@ this.PlacesUtils = {
if (itemId == PlacesUtils.placesRootId)
item.root = "placesRoot";
else if (itemId == PlacesUtils.bookmarksMenuFolderId)
item.root = "bookmarksMenuFolder";
else if (itemId == PlacesUtils.unfiledBookmarksFolderId)
item.root = "unfiledBookmarksFolder";
else if (itemId == PlacesUtils.toolbarFolderId)
item.root = "toolbarFolder";
+ else if (itemId == PlacesUtils.mobileFolderId)
+ item.root = "mobileFolder";
break;
case Ci.nsINavBookmarksService.TYPE_SEPARATOR:
item.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
break;
default:
Cu.reportError("Unexpected bookmark type");
break;
}
--- a/toolkit/components/places/nsINavBookmarksService.idl
+++ b/toolkit/components/places/nsINavBookmarksService.idl
@@ -276,16 +276,21 @@ interface nsINavBookmarksService : nsISu
readonly attribute long long unfiledBookmarksFolder;
/**
* The item ID of the personal toolbar folder.
*/
readonly attribute long long toolbarFolder;
/**
+ * The item ID of the mobile bookmarks folder.
+ */
+ readonly attribute long long mobileFolder;
+
+ /**
* This value should be used for APIs that allow passing in an index
* where an index is not known, or not required to be specified.
* e.g.: When appending an item to a folder.
*/
const short DEFAULT_INDEX = -1;
const unsigned short TYPE_BOOKMARK = 1;
const unsigned short TYPE_FOLDER = 2;
--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -115,16 +115,17 @@ private:
nsNavBookmarks::nsNavBookmarks()
: mItemCount(0)
, mRoot(0)
, mMenuRoot(0)
, mTagsRoot(0)
, mUnfiledRoot(0)
, mToolbarRoot(0)
+ , mMobileRoot(0)
, mCanNotify(false)
, mCacheObservers("bookmark-observers")
, mBatching(false)
{
NS_ASSERTION(!gBookmarksService,
"Attempting to create two instances of the service!");
gBookmarksService = this;
}
@@ -195,17 +196,17 @@ nsNavBookmarks::Init()
nsresult
nsNavBookmarks::ReadRoots()
{
nsCOMPtr<mozIStorageStatement> stmt;
nsresult rv = mDB->MainConn()->CreateStatement(NS_LITERAL_CSTRING(
"SELECT guid, id FROM moz_bookmarks WHERE guid IN ( "
"'root________', 'menu________', 'toolbar_____', "
- "'tags________', 'unfiled_____' )"
+ "'tags________', 'unfiled_____', 'mobile______' )"
), getter_AddRefs(stmt));
NS_ENSURE_SUCCESS(rv, rv);
bool hasResult;
while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
nsAutoCString guid;
rv = stmt->GetUTF8String(0, guid);
NS_ENSURE_SUCCESS(rv, rv);
@@ -223,19 +224,23 @@ nsNavBookmarks::ReadRoots()
mToolbarRoot = id;
}
else if (guid.EqualsLiteral("tags________")) {
mTagsRoot = id;
}
else if (guid.EqualsLiteral("unfiled_____")) {
mUnfiledRoot = id;
}
+ else if (guid.EqualsLiteral("mobile______")) {
+ mMobileRoot = id;
+ }
}
- if (!mRoot || !mMenuRoot || !mToolbarRoot || !mTagsRoot || !mUnfiledRoot)
+ if (!mRoot || !mMenuRoot || !mToolbarRoot || !mTagsRoot || !mUnfiledRoot ||
+ !mMobileRoot)
return NS_ERROR_FAILURE;
return NS_OK;
}
// nsNavBookmarks::IsBookmarkedInDatabase
//
// This checks to see if the specified place_id is actually bookmarked.
@@ -326,16 +331,24 @@ nsNavBookmarks::GetTagsFolder(int64_t* a
NS_IMETHODIMP
nsNavBookmarks::GetUnfiledBookmarksFolder(int64_t* aRoot)
{
*aRoot = mUnfiledRoot;
return NS_OK;
}
+NS_IMETHODIMP
+nsNavBookmarks::GetMobileFolder(int64_t* aRoot)
+{
+ *aRoot = mMobileRoot;
+ return NS_OK;
+}
+
+
nsresult
nsNavBookmarks::InsertBookmarkInDB(int64_t aPlaceId,
enum ItemType aItemType,
int64_t aParentId,
int32_t aIndex,
const nsACString& aTitle,
PRTime aDateAdded,
PRTime aLastModified,
--- a/toolkit/components/places/nsNavBookmarks.h
+++ b/toolkit/components/places/nsNavBookmarks.h
@@ -275,21 +275,22 @@ private:
nsMaybeWeakPtrArray<nsINavBookmarkObserver> mObservers;
int64_t mRoot;
int64_t mMenuRoot;
int64_t mTagsRoot;
int64_t mUnfiledRoot;
int64_t mToolbarRoot;
+ int64_t mMobileRoot;
inline bool IsRoot(int64_t aFolderId) {
return aFolderId == mRoot || aFolderId == mMenuRoot ||
aFolderId == mTagsRoot || aFolderId == mUnfiledRoot ||
- aFolderId == mToolbarRoot;
+ aFolderId == mToolbarRoot || aFolderId == mMobileRoot;
}
nsresult IsBookmarkedInDatabase(int64_t aBookmarkID, bool* aIsBookmarked);
nsresult SetItemDateInternal(enum mozilla::places::BookmarkDate aDateType,
int64_t aItemId,
PRTime aValue);
--- a/toolkit/components/places/nsNavHistoryQuery.cpp
+++ b/toolkit/components/places/nsNavHistoryQuery.cpp
@@ -169,17 +169,18 @@ inline void AppendInt64(nsACString& str,
}
namespace PlacesFolderConversion {
#define PLACES_ROOT_FOLDER "PLACES_ROOT"
#define BOOKMARKS_MENU_FOLDER "BOOKMARKS_MENU"
#define TAGS_FOLDER "TAGS"
#define UNFILED_BOOKMARKS_FOLDER "UNFILED_BOOKMARKS"
#define TOOLBAR_FOLDER "TOOLBAR"
-
+ #define MOBILE_BOOKMARKS_FOLDER "MOBILE_BOOKMARKS"
+
/**
* Converts a folder name to a folder id.
*
* @param aName
* The name of the folder to convert to a folder id.
* @returns the folder id if aName is a recognizable name, -1 otherwise.
*/
inline int64_t DecodeFolder(const nsCString &aName)
@@ -193,16 +194,18 @@ namespace PlacesFolderConversion {
else if (aName.EqualsLiteral(BOOKMARKS_MENU_FOLDER))
(void)bs->GetBookmarksMenuFolder(&folderID);
else if (aName.EqualsLiteral(TAGS_FOLDER))
(void)bs->GetTagsFolder(&folderID);
else if (aName.EqualsLiteral(UNFILED_BOOKMARKS_FOLDER))
(void)bs->GetUnfiledBookmarksFolder(&folderID);
else if (aName.EqualsLiteral(TOOLBAR_FOLDER))
(void)bs->GetToolbarFolder(&folderID);
+ else if (aName.EqualsLiteral(MOBILE_BOOKMARKS_FOLDER))
+ (void)bs->GetMobileFolder(&folderID);
return folderID;
}
/**
* Converts a folder id to a named constant, or a string representation of the
* folder id if there is no named constant for the folder, and appends it to
* aQuery.
@@ -234,16 +237,20 @@ namespace PlacesFolderConversion {
else if (NS_SUCCEEDED(bs->GetUnfiledBookmarksFolder(&folderID)) &&
aFolderID == folderID) {
aQuery.AppendLiteral(UNFILED_BOOKMARKS_FOLDER);
}
else if (NS_SUCCEEDED(bs->GetToolbarFolder(&folderID)) &&
aFolderID == folderID) {
aQuery.AppendLiteral(TOOLBAR_FOLDER);
}
+ else if (NS_SUCCEEDED(bs->GetMobileFolder(&folderID)) &&
+ aFolderID == folderID) {
+ aQuery.AppendLiteral(MOBILE_BOOKMARKS_FOLDER);
+ }
else {
// It wasn't one of our named constants, so just convert it to a string.
aQuery.AppendInt(aFolderID);
}
return NS_OK;
}
} // namespace PlacesFolderConversion
--- 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,17 +1,17 @@
/* -*- 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
-const PLACES_ROOTS_COUNT = 4;
+// Menu, Toolbar, Unsorted, Tags, Mobile
+const PLACES_ROOTS_COUNT = 5;
var tests = [];
/*
Backup/restore tests example:
var myTest = {
populate: function () { ... add bookmarks ... },
--- a/toolkit/components/places/tests/bookmarks/test_417228-other-roots.js
+++ b/toolkit/components/places/tests/bookmarks/test_417228-other-roots.js
@@ -20,49 +20,50 @@ this.push(myTest);
*/
tests.push({
excludeItemsFromRestore: [],
populate: function populate() {
// check initial size
var rootNode = PlacesUtils.getFolderContents(PlacesUtils.placesRootId,
false, false).root;
- do_check_eq(rootNode.childCount, 4);
+ do_check_eq(rootNode.childCount, 5);
// create a test root
this._folderTitle = "test folder";
this._folderId =
PlacesUtils.bookmarks.createFolder(PlacesUtils.placesRootId,
this._folderTitle,
PlacesUtils.bookmarks.DEFAULT_INDEX);
- do_check_eq(rootNode.childCount, 5);
+ do_check_eq(rootNode.childCount, 6);
// add a tag
this._testURI = PlacesUtils._uri("http://test");
this._tags = ["a", "b"];
PlacesUtils.tagging.tagURI(this._testURI, this._tags);
// add a child to each root, including our test root
this._roots = [PlacesUtils.bookmarksMenuFolderId, PlacesUtils.toolbarFolderId,
- PlacesUtils.unfiledBookmarksFolderId, this._folderId];
+ PlacesUtils.unfiledBookmarksFolderId, PlacesUtils.mobileFolderId,
+ this._folderId];
this._roots.forEach(function(aRootId) {
// clean slate
PlacesUtils.bookmarks.removeFolderChildren(aRootId);
// add a test bookmark
PlacesUtils.bookmarks.insertBookmark(aRootId, this._testURI,
PlacesUtils.bookmarks.DEFAULT_INDEX, "test");
}, this);
// add a folder to exclude from replacing during restore
// this will still be present post-restore
var excludedFolderId =
PlacesUtils.bookmarks.createFolder(PlacesUtils.placesRootId,
"excluded",
PlacesUtils.bookmarks.DEFAULT_INDEX);
- do_check_eq(rootNode.childCount, 6);
+ do_check_eq(rootNode.childCount, 7);
this.excludeItemsFromRestore.push(excludedFolderId);
// add a test bookmark to it
PlacesUtils.bookmarks.insertBookmark(excludedFolderId, this._testURI,
PlacesUtils.bookmarks.DEFAULT_INDEX, "test");
},
inbetween: function inbetween() {
@@ -85,17 +86,17 @@ tests.push({
var rootNode = PlacesUtils.getFolderContents(PlacesUtils.placesRootId,
false, false).root;
// validate litter is gone
do_check_neq(rootNode.getChild(0).title, this._litterTitle);
// test root count is the same
- do_check_eq(rootNode.childCount, 6);
+ do_check_eq(rootNode.childCount, 7);
var foundTestFolder = 0;
for (var i = 0; i < rootNode.childCount; i++) {
var node = rootNode.getChild(i);
do_print("validating " + node.title);
if (node.itemId != PlacesUtils.tagsFolderId) {
if (node.title == this._folderTitle) {
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_eraseEverything.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_eraseEverything.js
@@ -59,20 +59,24 @@ add_task(function* test_eraseEverything(
yield PlacesUtils.bookmarks.eraseEverything();
Assert.equal(frecencyForUrl("http://example.com/"), frecencyForExample);
Assert.equal(frecencyForUrl("http://example.com/"), frecencyForMozilla);
// Check there are no orphan annotations.
let conn = yield PlacesUtils.promiseDBConnection();
- let rows = yield conn.execute(`SELECT * FROM moz_items_annos`);
- Assert.equal(rows.length, 0);
- rows = yield conn.execute(`SELECT * FROM moz_anno_attributes`);
- Assert.equal(rows.length, 0);
+ let annoAttrs = yield conn.execute(`SELECT id, name FROM moz_anno_attributes`);
+ // Bug 1306445 will eventually remove the mobile root anno.
+ Assert.equal(annoAttrs.length, 1);
+ Assert.equal(annoAttrs[0].getResultByName("name"), PlacesUtils.MOBILE_ROOT_ANNO);
+ let annos = rows = yield conn.execute(`SELECT item_id, anno_attribute_id FROM moz_items_annos`);
+ Assert.equal(annos.length, 1);
+ Assert.equal(annos[0].getResultByName("item_id"), PlacesUtils.mobileFolderId);
+ Assert.equal(annos[0].getResultByName("anno_attribute_id"), annoAttrs[0].getResultByName("id"));
});
add_task(function* test_eraseEverything_roots() {
yield PlacesUtils.bookmarks.eraseEverything();
// Ensure the roots have not been removed.
Assert.ok(yield PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.unfiledGuid));
Assert.ok(yield PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.toolbarGuid));
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js
@@ -43,17 +43,18 @@ add_task(function* remove_nonexistent_gu
}
});
add_task(function* remove_roots_fail() {
let guids = [PlacesUtils.bookmarks.rootGuid,
PlacesUtils.bookmarks.unfiledGuid,
PlacesUtils.bookmarks.menuGuid,
PlacesUtils.bookmarks.toolbarGuid,
- PlacesUtils.bookmarks.tagsGuid];
+ PlacesUtils.bookmarks.tagsGuid,
+ PlacesUtils.bookmarks.mobileGuid];
for (let guid of guids) {
Assert.throws(() => PlacesUtils.bookmarks.remove(guid),
/It's not possible to remove Places root folders/);
}
});
add_task(function* remove_normal_folder_under_root_succeeds() {
let folder = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.rootGuid,
@@ -93,20 +94,24 @@ add_task(function* remove_bookmark_orpha
PlacesUtils.annotations.setItemAnnotation((yield PlacesUtils.promiseItemId(bm1.guid)),
"testanno", "testvalue", 0, 0);
let bm2 = yield PlacesUtils.bookmarks.remove(bm1.guid);
checkBookmarkObject(bm2);
// Check there are no orphan annotations.
let conn = yield PlacesUtils.promiseDBConnection();
- let rows = yield conn.execute(`SELECT * FROM moz_items_annos`);
- Assert.equal(rows.length, 0);
- rows = yield conn.execute(`SELECT * FROM moz_anno_attributes`);
- Assert.equal(rows.length, 0);
+ let annoAttrs = yield conn.execute(`SELECT id, name FROM moz_anno_attributes`);
+ // Bug 1306445 will eventually remove the mobile root anno.
+ Assert.equal(annoAttrs.length, 1);
+ Assert.equal(annoAttrs[0].getResultByName("name"), PlacesUtils.MOBILE_ROOT_ANNO);
+ let annos = rows = yield conn.execute(`SELECT item_id, anno_attribute_id FROM moz_items_annos`);
+ Assert.equal(annos.length, 1);
+ Assert.equal(annos[0].getResultByName("item_id"), PlacesUtils.mobileFolderId);
+ Assert.equal(annos[0].getResultByName("anno_attribute_id"), annoAttrs[0].getResultByName("id"));
});
add_task(function* remove_bookmark_empty_title() {
let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
url: "http://example.com/",
title: "" });
checkBookmarkObject(bm1);
--- a/toolkit/components/places/tests/bookmarks/test_protectRoots.js
+++ b/toolkit/components/places/tests/bookmarks/test_protectRoots.js
@@ -3,17 +3,18 @@
function run_test()
{
const ROOTS = [
PlacesUtils.bookmarksMenuFolderId,
PlacesUtils.toolbarFolderId,
PlacesUtils.unfiledBookmarksFolderId,
PlacesUtils.tagsFolderId,
- PlacesUtils.placesRootId
+ PlacesUtils.placesRootId,
+ PlacesUtils.mobileFolderId,
];
for (let root of ROOTS) {
do_check_true(PlacesUtils.isRootItem(root));
try {
PlacesUtils.bookmarks.removeItem(root);
do_throw("Trying to remove a root should throw");
--- a/toolkit/components/places/tests/expiration/test_annos_expire_session.js
+++ b/toolkit/components/places/tests/expiration/test_annos_expire_session.js
@@ -53,18 +53,20 @@ add_task(function* test_annos_expire_ses
items = as.getItemsWithAnnotation("test2");
do_check_eq(items.length, 10);
let deferred = Promise.defer();
waitForConnectionClosed(function() {
let stmt = DBConn(true).createAsyncStatement(
`SELECT id FROM moz_annos
UNION ALL
- SELECT id FROM moz_items_annos`
+ SELECT id FROM moz_items_annos
+ WHERE expiration = :expiration`
);
+ stmt.params.expiration = as.EXPIRE_SESSION;
stmt.executeAsync({
handleResult: function(aResultSet) {
dump_table("moz_annos");
dump_table("moz_items_annos");
do_throw("Should not find any leftover session annotations");
},
handleError: function(aError) {
do_throw("Error code " + aError.result + " with message '" +
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -1,14 +1,14 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
* 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 CURRENT_SCHEMA_VERSION = 34;
+const CURRENT_SCHEMA_VERSION = 35;
const FIRST_UPGRADABLE_SCHEMA_VERSION = 11;
const NS_APP_USER_PROFILE_50_DIR = "ProfD";
const NS_APP_PROFILE_DIR_STARTUP = "ProfDS";
// Shortcuts to transitions type.
const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK;
const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED;
new file mode 100644
index 0000000000000000000000000000000000000000..5e157d778091339a19f42bfa917be8635140b30f
GIT binary patch
literal 1146880
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
z0000000000000000000000000000000RCC^q{{|{&e}aQvb?M;94;AFR=gye?aUln
zQ`6nuJ*qvmD%sW+AC*cku1R!^TsXg`C7DjNq*l~)Cp(%`-I<Z2Yiqg_ZB426M9q<1
z+4hD^sw>@;IDBKVk&WqGp+x$?d{%p+IoZ{|-K?fmM>f%s&F5vZ>8_5Wd&|Uz$4=<&
zCpNlvRBUb{-I8d^#<Ho!iR_X@dQ|K;XI>ne)jP(;>C~urTU&Bfs(n;`=%tOJ;<^!I
z@7-rZ^US(=N7t`f-m<u^WBB0Rp5N=LX{7^0$At!!m6e6cLc_{SI@{t+i9_>$ZT|4_
z2WC>~?BS!w9@rjVF)xwnYRhI0&vkynxP_UiQ(NZDp1X8pDD>Lcg~vQR;_#Azp~>4c
zQ>Z_2tLk&=TbeS*&Y3%A=BhFI>f?WT!{<*Z9vE5}8kwseF3MGZkBN^rrn<5<TVG{y
zXl>h+MN`Mr&YiZhcJj*MgFpADsSPJb2ZkE{?uPSopH4P*WfR-99u2KM_M{0N6Hi(>
zf8n^+<Ax7z`RTgTn<4{4?SFUcee0#EE0ayNCs%FTNhGwkD?52)vg!DEOXt)%!v`Ny
zUVQ$kMFT@k|8OTu5^bH^v|kijo1E0$)ZQ?6LPMry`S8J!V{iS&Y2ksPWTE}?f8-)^
zYcw5i+op@q+Ho@`j$Ar_<x%P7t4_-G@a3N#TAABi;oy&<+z$W%000000000000000
z000000000000000000000DM5~94-x&g-goI%0k6;BgWpl&xXo;@|o}RumAu600000
z000000000000000000000000000000;C)t+Kb-kK4*~!H00000000000000000000
z0000000000000000N!VnxlcHFK9u_b00000000000000000000000000000000000
z004jwf?dNUq5X@>LhY$l$+oums8o7!=u;KBjBxOBDE9*Z00000000000000000000
z0000000000000000018x10(yFx2IMm+uGuzQt8E&xuij7+z|>M4!#(i7Syi!)0%Is
z3D!(r6J34p>PuFiy856q-pXYI000000000000000000000000000000000000095|
z4jnWg6b_Y#M}|TrCBurMvr>)8wnS`7D%IK^Pq$`5#fRTf{N%tDePMeczoFoef%&@O
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
z0000000000000000000000000000000002~=?#gD3%92llWmEb##E}cJ)UmO%uA)R
zk)e_L^1hV0=~QPTolPb(HO+}kQ##q1O{O~XRp++Fn{ui1bEUFVHkK}`%r|`I`+Q*l
z00000000000000000000000000000000000008hltIU0(!2zM*;$TT|c<uxM00000
z000000000000000000000000000000004HND!NlB91aaC3ztWugCe7&#ZL}CE&r*G
z6f%aEg~KH!l|}OsO^J@|g81T0G!iPV8&S9Z-#7GyhZGfx?q4W6plDVqlRZ9}NoEtx
z8_Ry~lvrQ5G&~>_E)Vte7LH7AZjLpkQmyUrbZaJ-O~tZHl9^abs;xPZ&POweL@p`O
z9&1i!I@{ta6V0(yM?NJsWmByMsZ?8IJiW26?jhIrg_XJM8@T$UQ1EbYUC<pI8yv9a
z?KKarxnfP@n%Xs?)%UKxB$o*Q000000000000000000000000000000000000Pz3h
z(1FpKSyk<+RSip$nQSV(a(Vvmy=S&bXy|N<Hzk_m*+jCVIk94NEcb6rWwhp~UH^`{
zE$LKyU$rp<{?1DM`x>=V^q|A`$d}plC+4?^G$gZ$jXjKw)y3*+E20M-`EKPq<LN|4
zwy(yR*yuycqX*4@uO8asnQVKiIoXm-H23RmOj-1x`rY59?&7Xwb3<20a#>fRuiEH?
zOQS;%E6hV-W;2`58d~DZlTE3PLeYt_x?v^Jq2sn+a!D$a-B@hwfyL3G6SiNhFr!VW
zu8wT3Ty-=$^za>YoK7?)I+|8)Y;w$~NOb7*?HA2YMWNYEw<*{3h@$Awqqbju>nWHR
z8(kgV;Xu05Z4FD}nI(NiD{?EfEcjU{cq{mG@JjH9;CI23!7p+N000000000000000
z000000000000000000000002+zg%%794;-(AB&6g$7nQvj70LsqM}HoygV8Sg-i2?
zl8RjUvY<N@ycxU}JQ>^@d?UCj_*5<d00000000000000000000000000000000000
z0002~zqeatWO#8pH7efLmRyx;AJyI6J*sSbiN)JXjBYP6vc1HjT_YpQw`nsnC_JjP
za4f0F4W=x3EEN1Tcs+O}cp-Q$csh6@mjD0&000000000000000000000000000000
z000000RIZ2k@9d^G*T8W%^ynghvNJpnm<JHhoVTNth^#uv@G~>D0nOQbMR8|`{3!|
zSHZ)%1ONa400000000000000000000000000000000000_+P6eQWOrYE$$7Xy+Ncm
zC@PK=mFMyz;c#PFG*T8W%^ylCa<$5W2SdSMgV%#sf)|44f~SKgatQzc0000000000
z00000000000000000000000000Pw%rfJjL=R9rV=?7jPJD2qly;nMt}B!4K*AENm~
zB!4K1L_*~ixhmn{l~C>n000000000000000000000000000000000000000!Kq{l*
zlJc@paovcq_wKWyB9{^l{u;{t000000000000000000000000000000000000002M
z2T~+bR$h?{!@+Bz+z$W%000000000000000000000000000000000000DPeA6e%eW
z71xazd+$CQDsu5b!52coo58EWAA)CsCxS<U2ZA33-wVDS+!EXrTo<g%Wdi^J00000
z000000000000000000000000000000!26>l8ZHl)Ru+Pi0fnG=r$P{|C<Kx6LQqsz
z94*fkDT#){;mE*3P&BAFDD4f3dxL0i5a|tyigQ7EQAMt+LBXY=;H}_K!HdCj!BfF6
zf`@|pg1dw71m6g54z3T@<+1?)000000000000000000000000000000000000N{O5
z8V!fTrIDR_!=j3kXt+FFTAmBa!lh+}Sfr#7iwr1~DJm*0q!tY<q!v|{M9ahB=%7Ln
zDJ}#>(c)-%u43+(D-n&Bm6ccI`Wh5ehk{pv-v&Pqei(c!SRY&&To{}kWP?O-d@wzj
zn9BwL000000000000000000000000000000000000DxWtqv4WpX{fkvMBV!5AE+!u
zOA678r!5&!h!z*3D;|4tr$RK^8@>Frib6Ef8(s89c_CU<h;~1_p{z1mUJ~w&T{EMk
zG8)Rq4!@)L$$={dZH(peHoUNC;1;o>0l8RtUu<*M>h?P$n`0|(TUWX{wtW1M;!Uy9
z*W9x%T9Lc7vf!Ff@TcJU;K|_O;GW=);A^=A000000000000000000000000000000
z000000002^XEPvD5)Or48@uqBhewq5MaKW~hR>H&M2f@12Y>EQQyYqVPg{Pv?(}Hy
z=^^FC=SO-^cYA)XtIEnF#pS~XN51^iLwge=$KLvlqKaJeWx>i&@KW&W;K#vtg0BRZ
z2j}M!000000000000000000000000000000000000002s{|`GyhK0MblSd|-j*qu=
zPMs6UC$vpjG<8hv+-WOoC$B8dCnP6zH?=p+ozReJSzeS+Xr5U&@96qf%Uc%Lb(G~3
zP8hc^Gj(dqoY`}ijx5b5%&Bi_${agq?wFaY#+2j}jy-8Y$HbFX&R;mL^|<Ju$guKp
zGbWB)I)CL+>E)|Vs>lsxP*4;KUJRZH?hn2l+!$OItPNHKOM(-Eql0n50l}`hYybcN
z00000000000000000000000000000000000{^ji)85ZuIdwga}Dw&+yy5yJv`Gl^H
zmSkI^xgoDOpODS8EU2%oZEZbnMQddtA(d)tjHe4FB87zb;!MLv(R@N?($vLs=cksY
zYuhH342ldZPp4AZz7qKY?Wx9GC%y4Og@i;$S6_jme8NfD1!EU1nYgrV#gg{Ye8TZl
znzNG|XQ!tnX0_~;PnbNp>%_%%Gp5(io0k}vPngscPn>YVvZL$gO_)}mPna+xxw7q~
zk*TE%>Lyj>6Y4ta7R_GTI(}7ib7NUWehSWfpKk#G0000000000000000000000000
z0000000000006wtD)NUj-{(O900000000000000000000000000000000000006-I
ztRnY`2EPdfZw9Xfzt5cj00000000000000000000000000000000000006+hq|#_O
Y93B*j7Q)J+NFf|hR8$-dmzPKWFS)u{#sB~S
--- a/toolkit/components/places/tests/migration/test_current_from_v24.js
+++ b/toolkit/components/places/tests/migration/test_current_from_v24.js
@@ -18,17 +18,18 @@ add_task(function* test_bookmark_guid_an
yield PlacesUtils.bookmarks.eraseEverything();
let db = yield PlacesUtils.promiseDBConnection();
let m = new Map([
[PlacesUtils.placesRootId, PlacesUtils.bookmarks.rootGuid],
[PlacesUtils.bookmarksMenuFolderId, PlacesUtils.bookmarks.menuGuid],
[PlacesUtils.toolbarFolderId, PlacesUtils.bookmarks.toolbarGuid],
[PlacesUtils.unfiledBookmarksFolderId, PlacesUtils.bookmarks.unfiledGuid],
- [PlacesUtils.tagsFolderId, PlacesUtils.bookmarks.tagsGuid]
+ [PlacesUtils.tagsFolderId, PlacesUtils.bookmarks.tagsGuid],
+ [PlacesUtils.mobileFolderId, PlacesUtils.bookmarks.mobileGuid],
]);
let rows = yield db.execute(`SELECT id, guid FROM moz_bookmarks`);
for (let row of rows) {
let id = row.getResultByName("id");
let guid = row.getResultByName("guid");
Assert.equal(m.get(id), guid, "The root folder has the correct GUID");
}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/migration/test_current_from_v34.js
@@ -0,0 +1,177 @@
+Cu.importGlobalProperties(["URL", "crypto"]);
+
+const { TYPE_BOOKMARK, TYPE_FOLDER } = Ci.nsINavBookmarksService;
+const { EXPIRE_NEVER, TYPE_INT32 } = Ci.nsIAnnotationService;
+
+function makeGuid() {
+ return ChromeUtils.base64URLEncode(crypto.getRandomValues(new Uint8Array(9)), {
+ pad: false,
+ });
+}
+
+// These queries are more or less copied directly from Bookmarks.jsm, but
+// operate on the old, pre-migration DB. We can't use any of the Places SQL
+// functions yet, because those are only registered for the main connection.
+function* insertItem(db, info) {
+ let [parentInfo] = yield db.execute(`
+ SELECT b.id, (SELECT count(*) FROM moz_bookmarks
+ WHERE parent = b.id) AS childCount
+ FROM moz_bookmarks b
+ WHERE b.guid = :parentGuid`,
+ { parentGuid: info.parentGuid });
+
+ let guid = makeGuid();
+ yield db.execute(`
+ INSERT INTO moz_bookmarks (fk, type, parent, position, guid)
+ VALUES ((SELECT id FROM moz_places WHERE url = :url),
+ :type, :parent, :position, :guid)`,
+ { url: info.url || "nonexistent", type: info.type, guid,
+ // Just append items.
+ position: parentInfo.getResultByName("childCount"),
+ parent: parentInfo.getResultByName("id") });
+
+ let id = (yield db.execute(`
+ SELECT id FROM moz_bookmarks WHERE guid = :guid LIMIT 1`,
+ { guid }))[0].getResultByName("id");
+
+ return { id, guid };
+}
+
+function insertBookmark(db, info) {
+ return db.executeTransaction(function* () {
+ if (info.type == TYPE_BOOKMARK) {
+ // We don't have access to the hash function here, so we omit the
+ // `url_hash` column. These will be fixed up automatically during
+ // migration.
+ let url = new URL(info.url);
+ let placeGuid = makeGuid();
+ yield db.execute(`
+ INSERT INTO moz_places (url, rev_host, hidden, frecency, guid)
+ VALUES (:url, :rev_host, 0, -1, :guid)`,
+ { url: url.href, guid: placeGuid,
+ rev_host: PlacesUtils.getReversedHost(url) });
+ }
+ return yield* insertItem(db, info);
+ });
+}
+
+function* insertAnno(db, itemId, name, value) {
+ yield db.execute(`INSERT OR IGNORE INTO moz_anno_attributes (name)
+ VALUES (:name)`, { name });
+ yield db.execute(`
+ INSERT INTO moz_items_annos
+ (item_id, anno_attribute_id, content, flags,
+ expiration, type, dateAdded, lastModified)
+ VALUES (:itemId,
+ (SELECT id FROM moz_anno_attributes
+ WHERE name = :name),
+ 1, 0, :expiration, :type, 0, 0)
+ `, { itemId, name, expiration: EXPIRE_NEVER, type: TYPE_INT32 });
+}
+
+function insertMobileFolder(db) {
+ return db.executeTransaction(function* () {
+ let item = yield* insertItem(db, {
+ type: TYPE_FOLDER,
+ parentGuid: "root________",
+ });
+ yield* insertAnno(db, item.id, "mobile/bookmarksRoot", 1);
+ return item;
+ });
+}
+
+function insertMobileQuery(db, parentGuid, folderId) {
+ return db.executeTransaction(function* () {
+ let item = yield* insertItem(db, {
+ type: TYPE_BOOKMARK,
+ parentGuid,
+ url: `place:folder=${folderId}`,
+ });
+ yield* insertAnno(db, item.id, "MobileBookmarks", 0);
+ return item;
+ });
+}
+
+var mobileId, mobileGuid, fxGuid, queryGuid;
+var dupeMobileId, dupeMobileGuid, tbGuid, dupeQueryGuid;
+
+add_task(function* setup() {
+ yield setupPlacesDatabase("places_v34.sqlite");
+ // Setup database contents to be migrated.
+ let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME);
+ let db = yield Sqlite.openConnection({ path });
+
+ do_print("Create mobile folder with bookmarks");
+ ({ id: mobileId, guid: mobileGuid } = yield insertMobileFolder(db));
+ ({ guid: fxGuid } = yield insertBookmark(db, {
+ type: TYPE_BOOKMARK,
+ url: "http://getfirefox.com",
+ parentGuid: mobileGuid,
+ }));
+
+ // We should only have one mobile folder, but, in case an old version of Sync
+ // did the wrong thing and created multiple mobile folders, we should merge
+ // their contents into the new mobile root.
+ do_print("Create second mobile folder with different bookmarks");
+ ({ id: dupeMobileId, guid: dupeMobileGuid } = yield insertMobileFolder(db));
+ ({ guid: tbGuid } = yield insertBookmark(db, {
+ type: TYPE_BOOKMARK,
+ url: "http://getthunderbird.com",
+ parentGuid: dupeMobileGuid,
+ }));
+
+ // Add queries that point to the old mobile folders. These should be
+ // deleted so that Sync can create a new one.
+ do_print("Insert query for mobile folder");
+ ({ guid: queryGuid} = yield insertMobileQuery(db, "toolbar_____", mobileId));
+
+ do_print("Insert query for second mobile folder");
+ ({ guid: dupeQueryGuid} = yield insertMobileQuery(db, "menu________",
+ dupeMobileId));
+
+ yield db.close();
+});
+
+add_task(function* database_is_valid() {
+ // Accessing the database for the first time triggers migration.
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_UPGRADED);
+
+ let db = yield PlacesUtils.promiseDBConnection();
+ Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
+});
+
+add_task(function* test_mobile_root() {
+ let fxBmk = yield PlacesUtils.bookmarks.fetch(fxGuid);
+ equal(fxBmk.parentGuid, PlacesUtils.bookmarks.mobileGuid,
+ "Firefox bookmark should be moved to new mobile root");
+ equal(fxBmk.index, 0, "Firefox bookmark should be first child of new root");
+
+ let tbBmk = yield PlacesUtils.bookmarks.fetch(tbGuid);
+ equal(tbBmk.parentGuid, PlacesUtils.bookmarks.mobileGuid,
+ "Thunderbird bookmark should be moved to new mobile root");
+ equal(tbBmk.index, 1,
+ "Thunderbird bookmark should be second child of new root");
+
+ let mobileRootId = PlacesUtils.promiseItemId(
+ PlacesUtils.bookmarks.mobileGuid);
+ let annoItemIds = PlacesUtils.annotations.getItemsWithAnnotation(
+ PlacesUtils.MOBILE_ROOT_ANNO, {});
+ deepEqual(annoItemIds, [mobileRootId],
+ "Only mobile root should have mobile anno");
+});
+
+add_task(function* test_mobile_queries() {
+ let mobileRootId = PlacesUtils.promiseItemId(
+ PlacesUtils.bookmarks.mobileGuid);
+
+ let query = yield PlacesUtils.bookmarks.fetch(queryGuid);
+ ok(!query, "Query should be removed");
+
+ let dupeQuery = yield PlacesUtils.bookmarks.fetch(dupeQueryGuid);
+ ok(!dupeQuery, "Dupe query should be removed");
+
+ let annoQueryIds = PlacesUtils.annotations.getItemsWithAnnotation(
+ "MobileBookmarks", {});
+ deepEqual(annoQueryIds, [], "All mobile query annos should be removed");
+});
--- a/toolkit/components/places/tests/migration/xpcshell.ini
+++ b/toolkit/components/places/tests/migration/xpcshell.ini
@@ -16,18 +16,20 @@ support-files =
places_v26.sqlite
places_v27.sqlite
places_v28.sqlite
places_v30.sqlite
places_v31.sqlite
places_v32.sqlite
places_v33.sqlite
places_v34.sqlite
+ places_v35.sqlite
[test_current_from_downgraded.js]
[test_current_from_v6.js]
[test_current_from_v11.js]
[test_current_from_v19.js]
[test_current_from_v24.js]
[test_current_from_v25.js]
[test_current_from_v26.js]
[test_current_from_v27.js]
[test_current_from_v31.js]
+[test_current_from_v34.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_import.json
@@ -0,0 +1,1 @@
+{"guid":"root________","title":"","index":0,"dateAdded":1475084731479000,"lastModified":1475084731479000,"id":1,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","index":0,"dateAdded":1475084731479000,"lastModified":1475084731768000,"id":2,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"X6lUyOspVYwi","title":"Test Pilot","index":0,"dateAdded":1475084731768000,"lastModified":1475084731768000,"id":3,"type":"text/x-moz-place","uri":"https://testpilot.firefox.com/"},{"guid":"XF4yRP6bTuil","title":"Mobile bookmarks query","index":1,"dateAdded":1475084731768000,"lastModified":1475084731768000,"id":11,"type":"text/x-moz-place","uri":"place:folder=101"}]},{"guid":"toolbar_____","title":"Bookmarks Toolbar","index":1,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":4,"type":"text/x-moz-place-container","root":"toolbarFolder","children":[{"guid":"buy7711R3ZgE","title":"MDN","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":5,"type":"text/x-moz-place","uri":"https://developer.mozilla.org"}]},{"guid":"3qmd_imziEBE","title":"Mobile Bookmarks","index":5,"dateAdded":1475084731479000,"lastModified":1475084731770000,"id":101,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1},{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"A description of the mobile folder that should be ignored on import"}],"type":"text/x-moz-place-container","children":[{"guid":"_o8e1_zxTJFg","title":"Get Firefox!","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":7,"type":"text/x-moz-place","uri":"http://getfirefox.com/"},{"guid":"QCtSqkVYUbXB","title":"Get Thunderbird!","index":1,"dateAdded":1475084731770000,"lastModified":1475084731770000,"id":8,"type":"text/x-moz-place","uri":"http://getthunderbird.com/"}]},{"guid":"unfiled_____","title":"Other Bookmarks","index":3,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":9,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder","children":[{"guid":"KIa9iKZab2Z5","title":"Add-ons","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":10,"type":"text/x-moz-place","uri":"https://addons.mozilla.org"}]}]}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_merge.json
@@ -0,0 +1,1 @@
+{"guid":"root________","title":"","index":0,"dateAdded":1475084731479000,"lastModified":1475084731479000,"id":1,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","index":0,"dateAdded":1475084731479000,"lastModified":1475084731768000,"id":2,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"Utodo9b0oVws","title":"Firefox Accounts","index":0,"dateAdded":1475084731955000,"lastModified":1475084731955000,"id":3,"type":"text/x-moz-place","uri":"https://accounts.firefox.com/"}]},{"guid":"toolbar_____","title":"Bookmarks Toolbar","index":1,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":4,"type":"text/x-moz-place-container","root":"toolbarFolder"},{"guid":"3qmd_imziEBE","title":"Mobile Bookmarks","index":5,"dateAdded":1475084731479000,"lastModified":1475084731770000,"id":5,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1},{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"A description of the mobile folder that should be ignored on import"}],"type":"text/x-moz-place-container","children":[{"guid":"a17yW6-nTxEJ","title":"Mozilla","index":0,"dateAdded":1475084731959000,"lastModified":1475084731959000,"id":6,"type":"text/x-moz-place","uri":"https://mozilla.org/"},{"guid":"xV10h9Wi3FBM","title":"Bugzilla","index":1,"dateAdded":1475084731961000,"lastModified":1475084731961000,"id":7,"type":"text/x-moz-place","uri":"https://bugzilla.mozilla.org/"}]},{"guid":"unfiled_____","title":"Other Bookmarks","index":3,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":8,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder"}]}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_multiple_folders.json
@@ -0,0 +1,1 @@
+{"guid":"root________","title":"","index":0,"dateAdded":1475084731479000,"lastModified":1475084731479000,"id":1,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","index":0,"dateAdded":1475084731479000,"lastModified":1475084731768000,"id":2,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"buy7711R3ZgE","title":"MDN","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":3,"type":"text/x-moz-place","uri":"https://developer.mozilla.org"},{"guid":"F_LBgd1fS_uQ","title":"Mobile bookmarks query for first folder","index":1,"dateAdded":1475084731768000,"lastModified":1475084731768000,"id":11,"type":"text/x-moz-place","uri":"place:folder=101"},{"guid":"oIpmQXMWsXvY","title":"Mobile bookmarks query for second folder","index":2,"dateAdded":1475084731768000,"lastModified":1475084731768000,"id":12,"type":"text/x-moz-place","uri":"place:folder=102"}]},{"guid":"3qmd_imziEBE","title":"Mobile Bookmarks","index":5,"dateAdded":1475084731479000,"lastModified":1475084731770000,"id":101,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1},{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"A description of the mobile folder that should be ignored on import"}],"type":"text/x-moz-place-container","children":[{"guid":"a17yW6-nTxEJ","title":"Mozilla","index":0,"dateAdded":1475084731959000,"lastModified":1475084731959000,"id":5,"type":"text/x-moz-place","uri":"https://mozilla.org/"}]},{"guid":"toolbar_____","title":"Bookmarks Toolbar","index":1,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":6,"type":"text/x-moz-place-container","root":"toolbarFolder","children":[{"guid":"Utodo9b0oVws","title":"Firefox Accounts","index":0,"dateAdded":1475084731955000,"lastModified":1475084731955000,"id":7,"type":"text/x-moz-place","uri":"https://accounts.firefox.com/"}]},{"guid":"o4YjJpgsufU-","title":"Mobile Bookmarks","index":7,"dateAdded":1475084731479000,"lastModified":1475084731770000,"id":102,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1}],"type":"text/x-moz-place-container","children":[{"guid":"sSZ86WT9WbN3","title":"DXR","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":9,"type":"text/x-moz-place","uri":"https://dxr.mozilla.org"}]},{"guid":"unfiled_____","title":"Other Bookmarks","index":3,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":10,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder","children":[{"guid":"xV10h9Wi3FBM","title":"Bugzilla","index":1,"dateAdded":1475084731961000,"lastModified":1475084731961000,"id":11,"type":"text/x-moz-place","uri":"https://bugzilla.mozilla.org/"}]}]}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_root_import.json
@@ -0,0 +1,1 @@
+{"guid":"root________","title":"","index":0,"dateAdded":1475084731479000,"lastModified":1475084731479000,"id":1,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","index":0,"dateAdded":1475084731479000,"lastModified":1475084731768000,"id":2,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"X6lUyOspVYwi","title":"Test Pilot","index":0,"dateAdded":1475084731768000,"lastModified":1475084731768000,"id":3,"type":"text/x-moz-place","uri":"https://testpilot.firefox.com/"}]},{"guid":"toolbar_____","title":"Bookmarks Toolbar","index":1,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":4,"type":"text/x-moz-place-container","root":"toolbarFolder"},{"guid":"unfiled_____","title":"Other Bookmarks","index":3,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":5,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder"},{"guid":"mobile______","title":"Mobile Bookmarks","index":4,"dateAdded":1475084731479000,"lastModified":1475084731770000,"id":6,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1}],"type":"text/x-moz-place-container","root":"mobileFolder","children":[{"guid":"_o8e1_zxTJFg","title":"Get Firefox!","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":7,"type":"text/x-moz-place","uri":"http://getfirefox.com/"},{"guid":"QCtSqkVYUbXB","title":"Get Thunderbird!","index":1,"dateAdded":1475084731770000,"lastModified":1475084731770000,"id":8,"type":"text/x-moz-place","uri":"http://getthunderbird.com/"}]}]}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_root_merge.json
@@ -0,0 +1,1 @@
+{"guid":"root________","title":"","index":0,"dateAdded":1475084731479000,"lastModified":1475084731479000,"id":1,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","index":0,"dateAdded":1475084731479000,"lastModified":1475084731955000,"id":2,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"Utodo9b0oVws","title":"Firefox Accounts","index":0,"dateAdded":1475084731955000,"lastModified":1475084731955000,"id":3,"type":"text/x-moz-place","uri":"https://accounts.firefox.com/"}]},{"guid":"toolbar_____","title":"Bookmarks Toolbar","index":1,"dateAdded":1475084731479000,"lastModified":1475084731938000,"id":4,"type":"text/x-moz-place-container","root":"toolbarFolder"},{"guid":"unfiled_____","title":"Other Bookmarks","index":3,"dateAdded":1475084731479000,"lastModified":1475084731938000,"id":5,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder"},{"guid":"mobile______","title":"Mobile Bookmarks","index":4,"dateAdded":1475084731479000,"lastModified":1475084731961000,"id":6,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1}],"type":"text/x-moz-place-container","root":"mobileFolder","children":[{"guid":"a17yW6-nTxEJ","title":"Mozilla","index":0,"dateAdded":1475084731959000,"lastModified":1475084731959000,"id":7,"type":"text/x-moz-place","uri":"https://mozilla.org/"},{"guid":"xV10h9Wi3FBM","title":"Bugzilla","index":1,"dateAdded":1475084731961000,"lastModified":1475084731961000,"id":8,"type":"text/x-moz-place","uri":"https://bugzilla.mozilla.org/"}]}]}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js
@@ -0,0 +1,292 @@
+function* importFromFixture(fixture, replace) {
+ let cwd = yield OS.File.getCurrentDirectory();
+ let path = OS.Path.join(cwd, fixture);
+
+ do_print(`Importing from ${path}`);
+ yield BookmarkJSONUtils.importFromFile(path, replace);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+}
+
+function* treeEquals(guid, expected, message) {
+ let root = yield PlacesUtils.promiseBookmarksTree(guid);
+ let bookmarks = (function nodeToEntry(node) {
+ let entry = { guid: node.guid, index: node.index }
+ if (node.children) {
+ entry.children = node.children.map(nodeToEntry);
+ }
+ if (node.annos) {
+ entry.annos = node.annos;
+ }
+ return entry;
+ }(root));
+
+ do_print(`Checking if ${guid} tree matches ${JSON.stringify(expected)}`);
+ do_print(`Got bookmarks tree for ${guid}: ${JSON.stringify(bookmarks)}`);
+
+ deepEqual(bookmarks, expected, message);
+}
+
+add_task(function* test_restore_mobile_bookmarks_root() {
+ yield* importFromFixture("mobile_bookmarks_root_import.json",
+ /* replace */ true);
+
+ yield* treeEquals(PlacesUtils.bookmarks.rootGuid, {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ index: 0,
+ children: [{
+ guid: PlacesUtils.bookmarks.menuGuid,
+ index: 0,
+ children: [
+ { guid: "X6lUyOspVYwi", index: 0 },
+ ],
+ }, {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ }, {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 3,
+ }, {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ index: 4,
+ annos: [{
+ name: "mobile/bookmarksRoot",
+ flags: 0,
+ expires: 4,
+ value: 1,
+ }],
+ children: [
+ { guid: "_o8e1_zxTJFg", index: 0 },
+ { guid: "QCtSqkVYUbXB", index: 1 },
+ ],
+ }],
+ }, "Should restore mobile bookmarks from root");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_import_mobile_bookmarks_root() {
+ yield* importFromFixture("mobile_bookmarks_root_import.json",
+ /* replace */ false);
+ yield* importFromFixture("mobile_bookmarks_root_merge.json",
+ /* replace */ false);
+
+ yield* 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: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ }, {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 3,
+ }, {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ 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 },
+ ],
+ }],
+ }, "Should merge bookmarks root contents");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_restore_mobile_bookmarks_folder() {
+ yield* importFromFixture("mobile_bookmarks_folder_import.json",
+ /* replace */ true);
+
+ yield* treeEquals(PlacesUtils.bookmarks.rootGuid, {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ index: 0,
+ children: [{
+ guid: PlacesUtils.bookmarks.menuGuid,
+ index: 0,
+ children: [
+ { guid: "X6lUyOspVYwi", index: 0 },
+ { guid: "XF4yRP6bTuil", index: 1 },
+ ],
+ }, {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ children: [{ guid: "buy7711R3ZgE", index: 0 }],
+ }, {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 3,
+ children: [{ guid: "KIa9iKZab2Z5", index: 0 }],
+ }, {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ index: 4,
+ annos: [{
+ name: "mobile/bookmarksRoot",
+ flags: 0,
+ expires: 4,
+ value: 1,
+ }],
+ children: [
+ { guid: "_o8e1_zxTJFg", index: 0 },
+ { guid: "QCtSqkVYUbXB", index: 1 },
+ ],
+ }],
+ }, "Should restore mobile bookmark folder contents into mobile root");
+
+ // We rewrite queries to point to the root ID instead of the name
+ // ("MOBILE_BOOKMARKS") so that we don't break them if the user downgrades
+ // to an earlier release channel. This can be removed along with the anno in
+ // bug 1306445.
+ let queryById = yield PlacesUtils.bookmarks.fetch("XF4yRP6bTuil");
+ equal(queryById.url.href, "place:folder=" + PlacesUtils.mobileFolderId,
+ "Should rewrite mobile query to point to root ID");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_import_mobile_bookmarks_folder() {
+ yield* importFromFixture("mobile_bookmarks_folder_import.json",
+ /* replace */ false);
+ yield* importFromFixture("mobile_bookmarks_folder_merge.json",
+ /* replace */ false);
+
+ yield* 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: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ children: [{ guid: "buy7711R3ZgE", index: 0 }],
+ }, {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 3,
+ children: [{ guid: "KIa9iKZab2Z5", index: 0 }],
+ }, {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ 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 },
+ ],
+ }],
+ }, "Should merge bookmarks folder contents into mobile root");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_restore_multiple_bookmarks_folders() {
+ yield* importFromFixture("mobile_bookmarks_multiple_folders.json",
+ /* replace */ true);
+
+ yield* 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: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ children: [{ guid: "Utodo9b0oVws", index: 0 }],
+ }, {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 3,
+ children: [{ guid: "xV10h9Wi3FBM", index: 0 }],
+ }, {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ index: 4,
+ annos: [{
+ name: "mobile/bookmarksRoot",
+ flags: 0,
+ expires: 4,
+ value: 1,
+ }],
+ children: [
+ { guid: "sSZ86WT9WbN3", index: 0 },
+ { guid: "a17yW6-nTxEJ", index: 1 },
+ ],
+ }],
+ }, "Should restore multiple bookmarks folder contents into root");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_import_multiple_bookmarks_folders() {
+ yield* importFromFixture("mobile_bookmarks_root_import.json",
+ /* replace */ false);
+ yield* importFromFixture("mobile_bookmarks_multiple_folders.json",
+ /* replace */ false);
+
+ yield* 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: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ children: [{ guid: "Utodo9b0oVws", index: 0 }],
+ }, {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 3,
+ children: [{ guid: "xV10h9Wi3FBM", index: 0 }],
+ }, {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ 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 },
+ ],
+ }],
+ }, "Should merge multiple mobile folders into root");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
--- a/toolkit/components/places/tests/unit/test_promiseBookmarksTree.js
+++ b/toolkit/components/places/tests/unit/test_promiseBookmarksTree.js
@@ -112,16 +112,19 @@ function* compareToNode(aItem, aNode, aI
compare_prop_to_value("root", "bookmarksMenuFolder");
break;
case PlacesUtils.toolbarFolderId:
compare_prop_to_value("root", "toolbarFolder");
break;
case PlacesUtils.unfiledBookmarksFolderId:
compare_prop_to_value("root", "unfiledBookmarksFolder");
break;
+ case PlacesUtils.mobileFolderId:
+ compare_prop_to_value("root", "mobileFolder");
+ break;
default:
check_unset("root");
}
break;
case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR:
do_check_eq(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR);
check_unset(...BOOKMARK_ONLY_PROPS, ...FOLDER_ONLY_PROPS);
break;
@@ -243,11 +246,11 @@ add_task(function* () {
let placesRootWithoutTheMenu =
yield test_promiseBookmarksTreeAgainstResult(PlacesUtils.bookmarks.rootGuid, {
excludeItemsCallback: aItem => {
guidsPassedToExcludeCallback.add(aItem.guid);
return aItem.root == "bookmarksMenuFolder";
},
includeItemIds: true
}, [PlacesUtils.bookmarks.menuGuid]);
- do_check_eq(guidsPassedToExcludeCallback.size, 4);
- do_check_eq(placesRootWithoutTheMenu.children.length, 2);
+ do_check_eq(guidsPassedToExcludeCallback.size, 5);
+ do_check_eq(placesRootWithoutTheMenu.children.length, 3);
});
--- a/toolkit/components/places/tests/unit/test_telemetry.js
+++ b/toolkit/components/places/tests/unit/test_telemetry.js
@@ -14,17 +14,19 @@ var histograms = {
PLACES_TAGGED_BOOKMARKS_PERC: val => do_check_eq(val, 100),
PLACES_DATABASE_FILESIZE_MB: val => do_check_true(val > 0),
PLACES_DATABASE_PAGESIZE_B: val => do_check_eq(val, 32768),
PLACES_DATABASE_SIZE_PER_PAGE_B: val => do_check_true(val > 0),
PLACES_EXPIRATION_STEPS_TO_CLEAN2: val => do_check_true(val > 1),
//PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS: val => do_check_true(val > 1),
PLACES_IDLE_FRECENCY_DECAY_TIME_MS: val => do_check_true(val > 0),
PLACES_IDLE_MAINTENANCE_TIME_MS: val => do_check_true(val > 0),
- PLACES_ANNOS_BOOKMARKS_COUNT: val => do_check_eq(val, 1),
+ // One from the `setItemAnnotation` call; the other from the mobile root.
+ // This can be removed along with the anno in bug 1306445.
+ PLACES_ANNOS_BOOKMARKS_COUNT: val => do_check_eq(val, 2),
PLACES_ANNOS_PAGES_COUNT: val => do_check_eq(val, 1),
PLACES_MAINTENANCE_DAYSFROMLAST: val => do_check_true(val >= 0),
}
/**
* Forces an expiration run.
*
* @param [optional] aLimit
--- a/toolkit/components/places/tests/unit/xpcshell.ini
+++ b/toolkit/components/places/tests/unit/xpcshell.ini
@@ -6,16 +6,21 @@ skip-if = toolkit == 'android' || toolki
support-files =
bookmarks.corrupt.html
bookmarks.json
bookmarks.preplaces.html
bookmarks_html_singleframe.html
bug476292.sqlite
default.sqlite
livemark.xml
+ mobile_bookmarks_folder_import.json
+ mobile_bookmarks_folder_merge.json
+ mobile_bookmarks_multiple_folders.json
+ mobile_bookmarks_root_import.json
+ mobile_bookmarks_root_merge.json
nsDummyObserver.js
nsDummyObserver.manifest
places.sparse.sqlite
[test_000_frecency.js]
[test_317472.js]
# Bug 676989: test hangs consistently on Android
skip-if = os == "android"
@@ -98,16 +103,17 @@ skip-if = os == "android"
[test_history_catobs.js]
[test_history_clear.js]
[test_history_notifications.js]
[test_history_observer.js]
# Bug 676989: test hangs consistently on Android
skip-if = os == "android"
[test_history_sidebar.js]
[test_hosts_triggers.js]
+[test_import_mobile_bookmarks.js]
[test_isPageInDB.js]
[test_isURIVisited.js]
[test_isvisited.js]
[test_keywords.js]
[test_lastModified.js]
[test_markpageas.js]
[test_mozIAsyncLivemarks.js]
[test_multi_queries.js]
--- a/toolkit/locales/en-US/chrome/places/places.properties
+++ b/toolkit/locales/en-US/chrome/places/places.properties
@@ -1,16 +1,17 @@
# 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/.
BookmarksMenuFolderTitle=Bookmarks Menu
BookmarksToolbarFolderTitle=Bookmarks Toolbar
OtherBookmarksFolderTitle=Other Bookmarks
TagsFolderTitle=Tags
+MobileBookmarksFolderTitle=Mobile Bookmarks
# LOCALIZATION NOTE (dateName):
# These are used to generate history containers when history is grouped by date
finduri-AgeInDays-is-0=Today
finduri-AgeInDays-is-1=Yesterday
finduri-AgeInDays-is=%S days ago
finduri-AgeInDays-last-is=Last %S days
finduri-AgeInDays-isgreater=Older than %S days