michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: this.EXPORTED_SYMBOLS = ['BookmarksEngine', "PlacesItem", "Bookmark", michael@0: "BookmarkFolder", "BookmarkQuery", michael@0: "Livemark", "BookmarkSeparator"]; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: michael@0: Cu.import("resource://gre/modules/PlacesUtils.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://services-common/async.js"); michael@0: Cu.import("resource://services-sync/constants.js"); michael@0: Cu.import("resource://services-sync/engines.js"); michael@0: Cu.import("resource://services-sync/record.js"); michael@0: Cu.import("resource://services-sync/util.js"); michael@0: Cu.import("resource://gre/modules/Task.jsm"); michael@0: Cu.import("resource://gre/modules/PlacesBackups.jsm"); michael@0: michael@0: const ALLBOOKMARKS_ANNO = "AllBookmarks"; michael@0: const DESCRIPTION_ANNO = "bookmarkProperties/description"; michael@0: const SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar"; michael@0: const MOBILEROOT_ANNO = "mobile/bookmarksRoot"; michael@0: const MOBILE_ANNO = "MobileBookmarks"; michael@0: const EXCLUDEBACKUP_ANNO = "places/excludeFromBackup"; michael@0: const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark"; michael@0: const PARENT_ANNO = "sync/parent"; michael@0: const ORGANIZERQUERY_ANNO = "PlacesOrganizer/OrganizerQuery"; michael@0: const ANNOS_TO_TRACK = [DESCRIPTION_ANNO, SIDEBAR_ANNO, michael@0: PlacesUtils.LMANNO_FEEDURI, PlacesUtils.LMANNO_SITEURI]; michael@0: michael@0: const SERVICE_NOT_SUPPORTED = "Service not supported on this platform"; michael@0: const FOLDER_SORTINDEX = 1000000; michael@0: michael@0: this.PlacesItem = function PlacesItem(collection, id, type) { michael@0: CryptoWrapper.call(this, collection, id); michael@0: this.type = type || "item"; michael@0: } michael@0: PlacesItem.prototype = { michael@0: decrypt: function PlacesItem_decrypt(keyBundle) { michael@0: // Do the normal CryptoWrapper decrypt, but change types before returning michael@0: let clear = CryptoWrapper.prototype.decrypt.call(this, keyBundle); michael@0: michael@0: // Convert the abstract places item to the actual object type michael@0: if (!this.deleted) michael@0: this.__proto__ = this.getTypeObject(this.type).prototype; michael@0: michael@0: return clear; michael@0: }, michael@0: michael@0: getTypeObject: function PlacesItem_getTypeObject(type) { michael@0: switch (type) { michael@0: case "bookmark": michael@0: case "microsummary": michael@0: return Bookmark; michael@0: case "query": michael@0: return BookmarkQuery; michael@0: case "folder": michael@0: return BookmarkFolder; michael@0: case "livemark": michael@0: return Livemark; michael@0: case "separator": michael@0: return BookmarkSeparator; michael@0: case "item": michael@0: return PlacesItem; michael@0: } michael@0: throw "Unknown places item object type: " + type; michael@0: }, michael@0: michael@0: __proto__: CryptoWrapper.prototype, michael@0: _logName: "Sync.Record.PlacesItem", michael@0: }; michael@0: michael@0: Utils.deferGetSet(PlacesItem, michael@0: "cleartext", michael@0: ["hasDupe", "parentid", "parentName", "type"]); michael@0: michael@0: this.Bookmark = function Bookmark(collection, id, type) { michael@0: PlacesItem.call(this, collection, id, type || "bookmark"); michael@0: } michael@0: Bookmark.prototype = { michael@0: __proto__: PlacesItem.prototype, michael@0: _logName: "Sync.Record.Bookmark", michael@0: }; michael@0: michael@0: Utils.deferGetSet(Bookmark, michael@0: "cleartext", michael@0: ["title", "bmkUri", "description", michael@0: "loadInSidebar", "tags", "keyword"]); michael@0: michael@0: this.BookmarkQuery = function BookmarkQuery(collection, id) { michael@0: Bookmark.call(this, collection, id, "query"); michael@0: } michael@0: BookmarkQuery.prototype = { michael@0: __proto__: Bookmark.prototype, michael@0: _logName: "Sync.Record.BookmarkQuery", michael@0: }; michael@0: michael@0: Utils.deferGetSet(BookmarkQuery, michael@0: "cleartext", michael@0: ["folderName", "queryId"]); michael@0: michael@0: this.BookmarkFolder = function BookmarkFolder(collection, id, type) { michael@0: PlacesItem.call(this, collection, id, type || "folder"); michael@0: } michael@0: BookmarkFolder.prototype = { michael@0: __proto__: PlacesItem.prototype, michael@0: _logName: "Sync.Record.Folder", michael@0: }; michael@0: michael@0: Utils.deferGetSet(BookmarkFolder, "cleartext", ["description", "title", michael@0: "children"]); michael@0: michael@0: this.Livemark = function Livemark(collection, id) { michael@0: BookmarkFolder.call(this, collection, id, "livemark"); michael@0: } michael@0: Livemark.prototype = { michael@0: __proto__: BookmarkFolder.prototype, michael@0: _logName: "Sync.Record.Livemark", michael@0: }; michael@0: michael@0: Utils.deferGetSet(Livemark, "cleartext", ["siteUri", "feedUri"]); michael@0: michael@0: this.BookmarkSeparator = function BookmarkSeparator(collection, id) { michael@0: PlacesItem.call(this, collection, id, "separator"); michael@0: } michael@0: BookmarkSeparator.prototype = { michael@0: __proto__: PlacesItem.prototype, michael@0: _logName: "Sync.Record.Separator", michael@0: }; michael@0: michael@0: Utils.deferGetSet(BookmarkSeparator, "cleartext", "pos"); michael@0: michael@0: michael@0: let kSpecialIds = { michael@0: michael@0: // Special IDs. Note that mobile can attempt to create a record on michael@0: // dereference; special accessors are provided to prevent recursion within michael@0: // observers. michael@0: guids: ["menu", "places", "tags", "toolbar", "unfiled", "mobile"], michael@0: michael@0: // Create the special mobile folder to store mobile bookmarks. michael@0: createMobileRoot: function createMobileRoot() { michael@0: let root = PlacesUtils.placesRootId; michael@0: let mRoot = PlacesUtils.bookmarks.createFolder(root, "mobile", -1); michael@0: PlacesUtils.annotations.setItemAnnotation( michael@0: mRoot, MOBILEROOT_ANNO, 1, 0, PlacesUtils.annotations.EXPIRE_NEVER); michael@0: PlacesUtils.annotations.setItemAnnotation( michael@0: mRoot, EXCLUDEBACKUP_ANNO, 1, 0, PlacesUtils.annotations.EXPIRE_NEVER); michael@0: return mRoot; michael@0: }, michael@0: michael@0: findMobileRoot: function findMobileRoot(create) { michael@0: // Use the (one) mobile root if it already exists. michael@0: let root = PlacesUtils.annotations.getItemsWithAnnotation(MOBILEROOT_ANNO, {}); michael@0: if (root.length != 0) michael@0: return root[0]; michael@0: michael@0: if (create) michael@0: return this.createMobileRoot(); michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: // Accessors for IDs. michael@0: isSpecialGUID: function isSpecialGUID(g) { michael@0: return this.guids.indexOf(g) != -1; michael@0: }, michael@0: michael@0: specialIdForGUID: function specialIdForGUID(guid, create) { michael@0: if (guid == "mobile") { michael@0: return this.findMobileRoot(create); michael@0: } michael@0: return this[guid]; michael@0: }, michael@0: michael@0: // Don't bother creating mobile: if it doesn't exist, this ID can't be it! michael@0: specialGUIDForId: function specialGUIDForId(id) { michael@0: for each (let guid in this.guids) michael@0: if (this.specialIdForGUID(guid, false) == id) michael@0: return guid; michael@0: return null; michael@0: }, michael@0: michael@0: get menu() PlacesUtils.bookmarksMenuFolderId, michael@0: get places() PlacesUtils.placesRootId, michael@0: get tags() PlacesUtils.tagsFolderId, michael@0: get toolbar() PlacesUtils.toolbarFolderId, michael@0: get unfiled() PlacesUtils.unfiledBookmarksFolderId, michael@0: get mobile() this.findMobileRoot(true), michael@0: }; michael@0: michael@0: this.BookmarksEngine = function BookmarksEngine(service) { michael@0: SyncEngine.call(this, "Bookmarks", service); michael@0: } michael@0: BookmarksEngine.prototype = { michael@0: __proto__: SyncEngine.prototype, michael@0: _recordObj: PlacesItem, michael@0: _storeObj: BookmarksStore, michael@0: _trackerObj: BookmarksTracker, michael@0: version: 2, michael@0: michael@0: _sync: function _sync() { michael@0: let engine = this; michael@0: let batchEx = null; michael@0: michael@0: // Try running sync in batch mode michael@0: PlacesUtils.bookmarks.runInBatchMode({ michael@0: runBatched: function wrappedSync() { michael@0: try { michael@0: SyncEngine.prototype._sync.call(engine); michael@0: } michael@0: catch(ex) { michael@0: batchEx = ex; michael@0: } michael@0: } michael@0: }, null); michael@0: michael@0: // Expose the exception if something inside the batch failed michael@0: if (batchEx != null) { michael@0: throw batchEx; michael@0: } michael@0: }, michael@0: michael@0: _guidMapFailed: false, michael@0: _buildGUIDMap: function _buildGUIDMap() { michael@0: let guidMap = {}; michael@0: for (let guid in this._store.getAllIDs()) { michael@0: // Figure out with which key to store the mapping. michael@0: let key; michael@0: let id = this._store.idForGUID(guid); michael@0: switch (PlacesUtils.bookmarks.getItemType(id)) { michael@0: case PlacesUtils.bookmarks.TYPE_BOOKMARK: michael@0: michael@0: // Smart bookmarks map to their annotation value. michael@0: let queryId; michael@0: try { michael@0: queryId = PlacesUtils.annotations.getItemAnnotation( michael@0: id, SMART_BOOKMARKS_ANNO); michael@0: } catch(ex) {} michael@0: michael@0: if (queryId) michael@0: key = "q" + queryId; michael@0: else michael@0: key = "b" + PlacesUtils.bookmarks.getBookmarkURI(id).spec + ":" + michael@0: PlacesUtils.bookmarks.getItemTitle(id); michael@0: break; michael@0: case PlacesUtils.bookmarks.TYPE_FOLDER: michael@0: key = "f" + PlacesUtils.bookmarks.getItemTitle(id); michael@0: break; michael@0: case PlacesUtils.bookmarks.TYPE_SEPARATOR: michael@0: key = "s" + PlacesUtils.bookmarks.getItemIndex(id); michael@0: break; michael@0: default: michael@0: continue; michael@0: } michael@0: michael@0: // The mapping is on a per parent-folder-name basis. michael@0: let parent = PlacesUtils.bookmarks.getFolderIdForItem(id); michael@0: if (parent <= 0) michael@0: continue; michael@0: michael@0: let parentName = PlacesUtils.bookmarks.getItemTitle(parent); michael@0: if (guidMap[parentName] == null) michael@0: guidMap[parentName] = {}; michael@0: michael@0: // If the entry already exists, remember that there are explicit dupes. michael@0: let entry = new String(guid); michael@0: entry.hasDupe = guidMap[parentName][key] != null; michael@0: michael@0: // Remember this item's GUID for its parent-name/key pair. michael@0: guidMap[parentName][key] = entry; michael@0: this._log.trace("Mapped: " + [parentName, key, entry, entry.hasDupe]); michael@0: } michael@0: michael@0: return guidMap; michael@0: }, michael@0: michael@0: // Helper function to get a dupe GUID for an item. michael@0: _mapDupe: function _mapDupe(item) { michael@0: // Figure out if we have something to key with. michael@0: let key; michael@0: let altKey; michael@0: switch (item.type) { michael@0: case "query": michael@0: // Prior to Bug 610501, records didn't carry their Smart Bookmark michael@0: // anno, so we won't be able to dupe them correctly. This altKey michael@0: // hack should get them to dupe correctly. michael@0: if (item.queryId) { michael@0: key = "q" + item.queryId; michael@0: altKey = "b" + item.bmkUri + ":" + item.title; michael@0: break; michael@0: } michael@0: // No queryID? Fall through to the regular bookmark case. michael@0: case "bookmark": michael@0: case "microsummary": michael@0: key = "b" + item.bmkUri + ":" + item.title; michael@0: break; michael@0: case "folder": michael@0: case "livemark": michael@0: key = "f" + item.title; michael@0: break; michael@0: case "separator": michael@0: key = "s" + item.pos; michael@0: break; michael@0: default: michael@0: return; michael@0: } michael@0: michael@0: // Figure out if we have a map to use! michael@0: // This will throw in some circumstances. That's fine. michael@0: let guidMap = this._guidMap; michael@0: michael@0: // Give the GUID if we have the matching pair. michael@0: this._log.trace("Finding mapping: " + item.parentName + ", " + key); michael@0: let parent = guidMap[item.parentName]; michael@0: michael@0: if (!parent) { michael@0: this._log.trace("No parent => no dupe."); michael@0: return undefined; michael@0: } michael@0: michael@0: let dupe = parent[key]; michael@0: michael@0: if (dupe) { michael@0: this._log.trace("Mapped dupe: " + dupe); michael@0: return dupe; michael@0: } michael@0: michael@0: if (altKey) { michael@0: dupe = parent[altKey]; michael@0: if (dupe) { michael@0: this._log.trace("Mapped dupe using altKey " + altKey + ": " + dupe); michael@0: return dupe; michael@0: } michael@0: } michael@0: michael@0: this._log.trace("No dupe found for key " + key + "/" + altKey + "."); michael@0: return undefined; michael@0: }, michael@0: michael@0: _syncStartup: function _syncStart() { michael@0: SyncEngine.prototype._syncStartup.call(this); michael@0: michael@0: let cb = Async.makeSpinningCallback(); michael@0: Task.spawn(function() { michael@0: // For first-syncs, make a backup for the user to restore michael@0: if (this.lastSync == 0) { michael@0: this._log.debug("Bookmarks backup starting."); michael@0: yield PlacesBackups.create(null, true); michael@0: this._log.debug("Bookmarks backup done."); michael@0: } michael@0: }.bind(this)).then( michael@0: cb, ex => { michael@0: // Failure to create a backup is somewhat bad, but probably not bad michael@0: // enough to prevent syncing of bookmarks - so just log the error and michael@0: // continue. michael@0: this._log.warn("Got exception \"" + Utils.exceptionStr(ex) + michael@0: "\" backing up bookmarks, but continuing with sync."); michael@0: cb(); michael@0: } michael@0: ); michael@0: michael@0: cb.wait(); michael@0: michael@0: this.__defineGetter__("_guidMap", function() { michael@0: // Create a mapping of folder titles and separator positions to GUID. michael@0: // We do this lazily so that we don't do any work unless we reconcile michael@0: // incoming items. michael@0: let guidMap; michael@0: try { michael@0: guidMap = this._buildGUIDMap(); michael@0: } catch (ex) { michael@0: this._log.warn("Got exception \"" + Utils.exceptionStr(ex) + michael@0: "\" building GUID map." + michael@0: " Skipping all other incoming items."); michael@0: throw {code: Engine.prototype.eEngineAbortApplyIncoming, michael@0: cause: ex}; michael@0: } michael@0: delete this._guidMap; michael@0: return this._guidMap = guidMap; michael@0: }); michael@0: michael@0: this._store._childrenToOrder = {}; michael@0: }, michael@0: michael@0: _processIncoming: function (newitems) { michael@0: try { michael@0: SyncEngine.prototype._processIncoming.call(this, newitems); michael@0: } finally { michael@0: // Reorder children. michael@0: this._tracker.ignoreAll = true; michael@0: this._store._orderChildren(); michael@0: this._tracker.ignoreAll = false; michael@0: delete this._store._childrenToOrder; michael@0: } michael@0: }, michael@0: michael@0: _syncFinish: function _syncFinish() { michael@0: SyncEngine.prototype._syncFinish.call(this); michael@0: this._tracker._ensureMobileQuery(); michael@0: }, michael@0: michael@0: _syncCleanup: function _syncCleanup() { michael@0: SyncEngine.prototype._syncCleanup.call(this); michael@0: delete this._guidMap; michael@0: }, michael@0: michael@0: _createRecord: function _createRecord(id) { michael@0: // Create the record as usual, but mark it as having dupes if necessary. michael@0: let record = SyncEngine.prototype._createRecord.call(this, id); michael@0: let entry = this._mapDupe(record); michael@0: if (entry != null && entry.hasDupe) { michael@0: record.hasDupe = true; michael@0: } michael@0: return record; michael@0: }, michael@0: michael@0: _findDupe: function _findDupe(item) { michael@0: this._log.trace("Finding dupe for " + item.id + michael@0: " (already duped: " + item.hasDupe + ")."); michael@0: michael@0: // Don't bother finding a dupe if the incoming item has duplicates. michael@0: if (item.hasDupe) { michael@0: this._log.trace(item.id + " already a dupe: not finding one."); michael@0: return; michael@0: } michael@0: let mapped = this._mapDupe(item); michael@0: this._log.debug(item.id + " mapped to " + mapped); michael@0: return mapped; michael@0: } michael@0: }; michael@0: michael@0: function BookmarksStore(name, engine) { michael@0: Store.call(this, name, engine); michael@0: michael@0: // Explicitly nullify our references to our cached services so we don't leak michael@0: Svc.Obs.add("places-shutdown", function() { michael@0: for each (let [query, stmt] in Iterator(this._stmts)) { michael@0: stmt.finalize(); michael@0: } michael@0: this._stmts = {}; michael@0: }, this); michael@0: } michael@0: BookmarksStore.prototype = { michael@0: __proto__: Store.prototype, michael@0: michael@0: itemExists: function BStore_itemExists(id) { michael@0: return this.idForGUID(id, true) > 0; michael@0: }, michael@0: michael@0: /* michael@0: * If the record is a tag query, rewrite it to refer to the local tag ID. michael@0: * michael@0: * Otherwise, just return. michael@0: */ michael@0: preprocessTagQuery: function preprocessTagQuery(record) { michael@0: if (record.type != "query" || michael@0: record.bmkUri == null || michael@0: !record.folderName) michael@0: return; michael@0: michael@0: // Yes, this works without chopping off the "place:" prefix. michael@0: let uri = record.bmkUri michael@0: let queriesRef = {}; michael@0: let queryCountRef = {}; michael@0: let optionsRef = {}; michael@0: PlacesUtils.history.queryStringToQueries(uri, queriesRef, queryCountRef, michael@0: optionsRef); michael@0: michael@0: // We only process tag URIs. michael@0: if (optionsRef.value.resultType != optionsRef.value.RESULTS_AS_TAG_CONTENTS) michael@0: return; michael@0: michael@0: // Tag something to ensure that the tag exists. michael@0: let tag = record.folderName; michael@0: let dummyURI = Utils.makeURI("about:weave#BStore_preprocess"); michael@0: PlacesUtils.tagging.tagURI(dummyURI, [tag]); michael@0: michael@0: // Look for the id of the tag, which might just have been added. michael@0: let tags = this._getNode(PlacesUtils.tagsFolderId); michael@0: if (!(tags instanceof Ci.nsINavHistoryQueryResultNode)) { michael@0: this._log.debug("tags isn't an nsINavHistoryQueryResultNode; aborting."); michael@0: return; michael@0: } michael@0: michael@0: tags.containerOpen = true; michael@0: try { michael@0: for (let i = 0; i < tags.childCount; i++) { michael@0: let child = tags.getChild(i); michael@0: if (child.title == tag) { michael@0: // Found the tag, so fix up the query to use the right id. michael@0: this._log.debug("Tag query folder: " + tag + " = " + child.itemId); michael@0: michael@0: this._log.trace("Replacing folders in: " + uri); michael@0: for each (let q in queriesRef.value) michael@0: q.setFolders([child.itemId], 1); michael@0: michael@0: record.bmkUri = PlacesUtils.history.queriesToQueryString( michael@0: queriesRef.value, queryCountRef.value, optionsRef.value); michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: finally { michael@0: tags.containerOpen = false; michael@0: } michael@0: }, michael@0: michael@0: applyIncoming: function BStore_applyIncoming(record) { michael@0: this._log.debug("Applying record " + record.id); michael@0: let isSpecial = record.id in kSpecialIds; michael@0: michael@0: if (record.deleted) { michael@0: if (isSpecial) { michael@0: this._log.warn("Ignoring deletion for special record " + record.id); michael@0: return; michael@0: } michael@0: michael@0: // Don't bother with pre and post-processing for deletions. michael@0: Store.prototype.applyIncoming.call(this, record); michael@0: return; michael@0: } michael@0: michael@0: // For special folders we're only interested in child ordering. michael@0: if (isSpecial && record.children) { michael@0: this._log.debug("Processing special node: " + record.id); michael@0: // Reorder children later michael@0: this._childrenToOrder[record.id] = record.children; michael@0: return; michael@0: } michael@0: michael@0: // Skip malformed records. (Bug 806460.) michael@0: if (record.type == "query" && michael@0: !record.bmkUri) { michael@0: this._log.warn("Skipping malformed query bookmark: " + record.id); michael@0: return; michael@0: } michael@0: michael@0: // Preprocess the record before doing the normal apply. michael@0: this.preprocessTagQuery(record); michael@0: michael@0: // Figure out the local id of the parent GUID if available michael@0: let parentGUID = record.parentid; michael@0: if (!parentGUID) { michael@0: throw "Record " + record.id + " has invalid parentid: " + parentGUID; michael@0: } michael@0: this._log.debug("Local parent is " + parentGUID); michael@0: michael@0: let parentId = this.idForGUID(parentGUID); michael@0: if (parentId > 0) { michael@0: // Save the parent id for modifying the bookmark later michael@0: record._parent = parentId; michael@0: record._orphan = false; michael@0: this._log.debug("Record " + record.id + " is not an orphan."); michael@0: } else { michael@0: this._log.trace("Record " + record.id + michael@0: " is an orphan: could not find parent " + parentGUID); michael@0: record._orphan = true; michael@0: } michael@0: michael@0: // Do the normal processing of incoming records michael@0: Store.prototype.applyIncoming.call(this, record); michael@0: michael@0: // Do some post-processing if we have an item michael@0: let itemId = this.idForGUID(record.id); michael@0: if (itemId > 0) { michael@0: // Move any children that are looking for this folder as a parent michael@0: if (record.type == "folder") { michael@0: this._reparentOrphans(itemId); michael@0: // Reorder children later michael@0: if (record.children) michael@0: this._childrenToOrder[record.id] = record.children; michael@0: } michael@0: michael@0: // Create an annotation to remember that it needs reparenting. michael@0: if (record._orphan) { michael@0: PlacesUtils.annotations.setItemAnnotation( michael@0: itemId, PARENT_ANNO, parentGUID, 0, michael@0: PlacesUtils.annotations.EXPIRE_NEVER); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Find all ids of items that have a given value for an annotation michael@0: */ michael@0: _findAnnoItems: function BStore__findAnnoItems(anno, val) { michael@0: return PlacesUtils.annotations.getItemsWithAnnotation(anno, {}) michael@0: .filter(function(id) { michael@0: return PlacesUtils.annotations.getItemAnnotation(id, anno) == val; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * For the provided parent item, attach its children to it michael@0: */ michael@0: _reparentOrphans: function _reparentOrphans(parentId) { michael@0: // Find orphans and reunite with this folder parent michael@0: let parentGUID = this.GUIDForId(parentId); michael@0: let orphans = this._findAnnoItems(PARENT_ANNO, parentGUID); michael@0: michael@0: this._log.debug("Reparenting orphans " + orphans + " to " + parentId); michael@0: orphans.forEach(function(orphan) { michael@0: // Move the orphan to the parent and drop the missing parent annotation michael@0: if (this._reparentItem(orphan, parentId)) { michael@0: PlacesUtils.annotations.removeItemAnnotation(orphan, PARENT_ANNO); michael@0: } michael@0: }, this); michael@0: }, michael@0: michael@0: _reparentItem: function _reparentItem(itemId, parentId) { michael@0: this._log.trace("Attempting to move item " + itemId + " to new parent " + michael@0: parentId); michael@0: try { michael@0: if (parentId > 0) { michael@0: PlacesUtils.bookmarks.moveItem(itemId, parentId, michael@0: PlacesUtils.bookmarks.DEFAULT_INDEX); michael@0: return true; michael@0: } michael@0: } catch(ex) { michael@0: this._log.debug("Failed to reparent item. " + Utils.exceptionStr(ex)); michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: // Turn a record's nsINavBookmarksService constant and other attributes into michael@0: // a granular type for comparison. michael@0: _recordType: function _recordType(itemId) { michael@0: let bms = PlacesUtils.bookmarks; michael@0: let type = bms.getItemType(itemId); michael@0: michael@0: switch (type) { michael@0: case bms.TYPE_FOLDER: michael@0: if (PlacesUtils.annotations michael@0: .itemHasAnnotation(itemId, PlacesUtils.LMANNO_FEEDURI)) { michael@0: return "livemark"; michael@0: } michael@0: return "folder"; michael@0: michael@0: case bms.TYPE_BOOKMARK: michael@0: let bmkUri = bms.getBookmarkURI(itemId).spec; michael@0: if (bmkUri.indexOf("place:") == 0) { michael@0: return "query"; michael@0: } michael@0: return "bookmark"; michael@0: michael@0: case bms.TYPE_SEPARATOR: michael@0: return "separator"; michael@0: michael@0: default: michael@0: return null; michael@0: } michael@0: }, michael@0: michael@0: create: function BStore_create(record) { michael@0: // Default to unfiled if we don't have the parent yet. michael@0: michael@0: // Valid parent IDs are all positive integers. Other values -- undefined, michael@0: // null, -1 -- all compare false for > 0, so this catches them all. We michael@0: // don't just use <= without the !, because undefined and null compare michael@0: // false for that, too! michael@0: if (!(record._parent > 0)) { michael@0: this._log.debug("Parent is " + record._parent + "; reparenting to unfiled."); michael@0: record._parent = kSpecialIds.unfiled; michael@0: } michael@0: michael@0: let newId; michael@0: switch (record.type) { michael@0: case "bookmark": michael@0: case "query": michael@0: case "microsummary": { michael@0: let uri = Utils.makeURI(record.bmkUri); michael@0: newId = PlacesUtils.bookmarks.insertBookmark( michael@0: record._parent, uri, PlacesUtils.bookmarks.DEFAULT_INDEX, record.title); michael@0: this._log.debug("created bookmark " + newId + " under " + record._parent michael@0: + " as " + record.title + " " + record.bmkUri); michael@0: michael@0: // Smart bookmark annotations are strings. michael@0: if (record.queryId) { michael@0: PlacesUtils.annotations.setItemAnnotation( michael@0: newId, SMART_BOOKMARKS_ANNO, record.queryId, 0, michael@0: PlacesUtils.annotations.EXPIRE_NEVER); michael@0: } michael@0: michael@0: if (Array.isArray(record.tags)) { michael@0: this._tagURI(uri, record.tags); michael@0: } michael@0: PlacesUtils.bookmarks.setKeywordForBookmark(newId, record.keyword); michael@0: if (record.description) { michael@0: PlacesUtils.annotations.setItemAnnotation( michael@0: newId, DESCRIPTION_ANNO, record.description, 0, michael@0: PlacesUtils.annotations.EXPIRE_NEVER); michael@0: } michael@0: michael@0: if (record.loadInSidebar) { michael@0: PlacesUtils.annotations.setItemAnnotation( michael@0: newId, SIDEBAR_ANNO, true, 0, michael@0: PlacesUtils.annotations.EXPIRE_NEVER); michael@0: } michael@0: michael@0: } break; michael@0: case "folder": michael@0: newId = PlacesUtils.bookmarks.createFolder( michael@0: record._parent, record.title, PlacesUtils.bookmarks.DEFAULT_INDEX); michael@0: this._log.debug("created folder " + newId + " under " + record._parent michael@0: + " as " + record.title); michael@0: michael@0: if (record.description) { michael@0: PlacesUtils.annotations.setItemAnnotation( michael@0: newId, DESCRIPTION_ANNO, record.description, 0, michael@0: PlacesUtils.annotations.EXPIRE_NEVER); michael@0: } michael@0: michael@0: // record.children will be dealt with in _orderChildren. michael@0: break; michael@0: case "livemark": michael@0: let siteURI = null; michael@0: if (!record.feedUri) { michael@0: this._log.debug("No feed URI: skipping livemark record " + record.id); michael@0: return; michael@0: } michael@0: if (PlacesUtils.annotations michael@0: .itemHasAnnotation(record._parent, PlacesUtils.LMANNO_FEEDURI)) { michael@0: this._log.debug("Invalid parent: skipping livemark record " + record.id); michael@0: return; michael@0: } michael@0: michael@0: if (record.siteUri != null) michael@0: siteURI = Utils.makeURI(record.siteUri); michael@0: michael@0: // Until this engine can handle asynchronous error reporting, we need to michael@0: // detect errors on creation synchronously. michael@0: let spinningCb = Async.makeSpinningCallback(); michael@0: michael@0: let livemarkObj = {title: record.title, michael@0: parentId: record._parent, michael@0: index: PlacesUtils.bookmarks.DEFAULT_INDEX, michael@0: feedURI: Utils.makeURI(record.feedUri), michael@0: siteURI: siteURI, michael@0: guid: record.id}; michael@0: PlacesUtils.livemarks.addLivemark(livemarkObj).then( michael@0: aLivemark => { spinningCb(null, [Components.results.NS_OK, aLivemark]) }, michael@0: () => { spinningCb(null, [Components.results.NS_ERROR_UNEXPECTED, aLivemark]) } michael@0: ); michael@0: michael@0: let [status, livemark] = spinningCb.wait(); michael@0: if (!Components.isSuccessCode(status)) { michael@0: throw status; michael@0: } michael@0: michael@0: this._log.debug("Created livemark " + livemark.id + " under " + michael@0: livemark.parentId + " as " + livemark.title + michael@0: ", " + livemark.siteURI.spec + ", " + michael@0: livemark.feedURI.spec + ", GUID " + michael@0: livemark.guid); michael@0: break; michael@0: case "separator": michael@0: newId = PlacesUtils.bookmarks.insertSeparator( michael@0: record._parent, PlacesUtils.bookmarks.DEFAULT_INDEX); michael@0: this._log.debug("created separator " + newId + " under " + record._parent); michael@0: break; michael@0: case "item": michael@0: this._log.debug(" -> got a generic places item.. do nothing?"); michael@0: return; michael@0: default: michael@0: this._log.error("_create: Unknown item type: " + record.type); michael@0: return; michael@0: } michael@0: michael@0: if (newId) { michael@0: // Livemarks can set the GUID through the API, so there's no need to michael@0: // do that here. michael@0: this._log.trace("Setting GUID of new item " + newId + " to " + record.id); michael@0: this._setGUID(newId, record.id); michael@0: } michael@0: }, michael@0: michael@0: // Factored out of `remove` to avoid redundant DB queries when the Places ID michael@0: // is already known. michael@0: removeById: function removeById(itemId, guid) { michael@0: let type = PlacesUtils.bookmarks.getItemType(itemId); michael@0: michael@0: switch (type) { michael@0: case PlacesUtils.bookmarks.TYPE_BOOKMARK: michael@0: this._log.debug(" -> removing bookmark " + guid); michael@0: PlacesUtils.bookmarks.removeItem(itemId); michael@0: break; michael@0: case PlacesUtils.bookmarks.TYPE_FOLDER: michael@0: this._log.debug(" -> removing folder " + guid); michael@0: PlacesUtils.bookmarks.removeItem(itemId); michael@0: break; michael@0: case PlacesUtils.bookmarks.TYPE_SEPARATOR: michael@0: this._log.debug(" -> removing separator " + guid); michael@0: PlacesUtils.bookmarks.removeItem(itemId); michael@0: break; michael@0: default: michael@0: this._log.error("remove: Unknown item type: " + type); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: remove: function BStore_remove(record) { michael@0: if (kSpecialIds.isSpecialGUID(record.id)) { michael@0: this._log.warn("Refusing to remove special folder " + record.id); michael@0: return; michael@0: } michael@0: michael@0: let itemId = this.idForGUID(record.id); michael@0: if (itemId <= 0) { michael@0: this._log.debug("Item " + record.id + " already removed"); michael@0: return; michael@0: } michael@0: this.removeById(itemId, record.id); michael@0: }, michael@0: michael@0: _taggableTypes: ["bookmark", "microsummary", "query"], michael@0: isTaggable: function isTaggable(recordType) { michael@0: return this._taggableTypes.indexOf(recordType) != -1; michael@0: }, michael@0: michael@0: update: function BStore_update(record) { michael@0: let itemId = this.idForGUID(record.id); michael@0: michael@0: if (itemId <= 0) { michael@0: this._log.debug("Skipping update for unknown item: " + record.id); michael@0: return; michael@0: } michael@0: michael@0: // Two items are the same type if they have the same ItemType in Places, michael@0: // and also share some key characteristics (e.g., both being livemarks). michael@0: // We figure this out by examining the item to find the equivalent granular michael@0: // (string) type. michael@0: // If they're not the same type, we can't just update attributes. Delete michael@0: // then recreate the record instead. michael@0: let localItemType = this._recordType(itemId); michael@0: let remoteRecordType = record.type; michael@0: this._log.trace("Local type: " + localItemType + ". " + michael@0: "Remote type: " + remoteRecordType + "."); michael@0: michael@0: if (localItemType != remoteRecordType) { michael@0: this._log.debug("Local record and remote record differ in type. " + michael@0: "Deleting and recreating."); michael@0: this.removeById(itemId, record.id); michael@0: this.create(record); michael@0: return; michael@0: } michael@0: michael@0: this._log.trace("Updating " + record.id + " (" + itemId + ")"); michael@0: michael@0: // Move the bookmark to a new parent or new position if necessary michael@0: if (record._parent > 0 && michael@0: PlacesUtils.bookmarks.getFolderIdForItem(itemId) != record._parent) { michael@0: this._reparentItem(itemId, record._parent); michael@0: } michael@0: michael@0: for (let [key, val] in Iterator(record.cleartext)) { michael@0: switch (key) { michael@0: case "title": michael@0: PlacesUtils.bookmarks.setItemTitle(itemId, val); michael@0: break; michael@0: case "bmkUri": michael@0: PlacesUtils.bookmarks.changeBookmarkURI(itemId, Utils.makeURI(val)); michael@0: break; michael@0: case "tags": michael@0: if (Array.isArray(val)) { michael@0: if (this.isTaggable(remoteRecordType)) { michael@0: this._tagID(itemId, val); michael@0: } else { michael@0: this._log.debug("Remote record type is invalid for tags: " + remoteRecordType); michael@0: } michael@0: } michael@0: break; michael@0: case "keyword": michael@0: PlacesUtils.bookmarks.setKeywordForBookmark(itemId, val); michael@0: break; michael@0: case "description": michael@0: if (val) { michael@0: PlacesUtils.annotations.setItemAnnotation( michael@0: itemId, DESCRIPTION_ANNO, val, 0, michael@0: PlacesUtils.annotations.EXPIRE_NEVER); michael@0: } else { michael@0: PlacesUtils.annotations.removeItemAnnotation(itemId, DESCRIPTION_ANNO); michael@0: } michael@0: break; michael@0: case "loadInSidebar": michael@0: if (val) { michael@0: PlacesUtils.annotations.setItemAnnotation( michael@0: itemId, SIDEBAR_ANNO, true, 0, michael@0: PlacesUtils.annotations.EXPIRE_NEVER); michael@0: } else { michael@0: PlacesUtils.annotations.removeItemAnnotation(itemId, SIDEBAR_ANNO); michael@0: } michael@0: break; michael@0: case "queryId": michael@0: PlacesUtils.annotations.setItemAnnotation( michael@0: itemId, SMART_BOOKMARKS_ANNO, val, 0, michael@0: PlacesUtils.annotations.EXPIRE_NEVER); michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: _orderChildren: function _orderChildren() { michael@0: for (let [guid, children] in Iterator(this._childrenToOrder)) { michael@0: // Reorder children according to the GUID list. Gracefully deal michael@0: // with missing items, e.g. locally deleted. michael@0: let delta = 0; michael@0: let parent = null; michael@0: for (let idx = 0; idx < children.length; idx++) { michael@0: let itemid = this.idForGUID(children[idx]); michael@0: if (itemid == -1) { michael@0: delta += 1; michael@0: this._log.trace("Could not locate record " + children[idx]); michael@0: continue; michael@0: } michael@0: try { michael@0: // This code path could be optimized by caching the parent earlier. michael@0: // Doing so should take in count any edge case due to reparenting michael@0: // or parent invalidations though. michael@0: if (!parent) { michael@0: parent = PlacesUtils.bookmarks.getFolderIdForItem(itemid); michael@0: } michael@0: PlacesUtils.bookmarks.moveItem(itemid, parent, idx - delta); michael@0: } catch (ex) { michael@0: this._log.debug("Could not move item " + children[idx] + ": " + ex); michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: changeItemID: function BStore_changeItemID(oldID, newID) { michael@0: this._log.debug("Changing GUID " + oldID + " to " + newID); michael@0: michael@0: // Make sure there's an item to change GUIDs michael@0: let itemId = this.idForGUID(oldID); michael@0: if (itemId <= 0) michael@0: return; michael@0: michael@0: this._setGUID(itemId, newID); michael@0: }, michael@0: michael@0: _getNode: function BStore__getNode(folder) { michael@0: let query = PlacesUtils.history.getNewQuery(); michael@0: query.setFolders([folder], 1); michael@0: return PlacesUtils.history.executeQuery( michael@0: query, PlacesUtils.history.getNewQueryOptions()).root; michael@0: }, michael@0: michael@0: _getTags: function BStore__getTags(uri) { michael@0: try { michael@0: if (typeof(uri) == "string") michael@0: uri = Utils.makeURI(uri); michael@0: } catch(e) { michael@0: this._log.warn("Could not parse URI \"" + uri + "\": " + e); michael@0: } michael@0: return PlacesUtils.tagging.getTagsForURI(uri, {}); michael@0: }, michael@0: michael@0: _getDescription: function BStore__getDescription(id) { michael@0: try { michael@0: return PlacesUtils.annotations.getItemAnnotation(id, DESCRIPTION_ANNO); michael@0: } catch (e) { michael@0: return null; michael@0: } michael@0: }, michael@0: michael@0: _isLoadInSidebar: function BStore__isLoadInSidebar(id) { michael@0: return PlacesUtils.annotations.itemHasAnnotation(id, SIDEBAR_ANNO); michael@0: }, michael@0: michael@0: get _childGUIDsStm() { michael@0: return this._getStmt( michael@0: "SELECT id AS item_id, guid " + michael@0: "FROM moz_bookmarks " + michael@0: "WHERE parent = :parent " + michael@0: "ORDER BY position"); michael@0: }, michael@0: _childGUIDsCols: ["item_id", "guid"], michael@0: michael@0: _getChildGUIDsForId: function _getChildGUIDsForId(itemid) { michael@0: let stmt = this._childGUIDsStm; michael@0: stmt.params.parent = itemid; michael@0: let rows = Async.querySpinningly(stmt, this._childGUIDsCols); michael@0: return rows.map(function (row) { michael@0: if (row.guid) { michael@0: return row.guid; michael@0: } michael@0: // A GUID hasn't been assigned to this item yet, do this now. michael@0: return this.GUIDForId(row.item_id); michael@0: }, this); michael@0: }, michael@0: michael@0: // Create a record starting from the weave id (places guid) michael@0: createRecord: function createRecord(id, collection) { michael@0: let placeId = this.idForGUID(id); michael@0: let record; michael@0: if (placeId <= 0) { // deleted item michael@0: record = new PlacesItem(collection, id); michael@0: record.deleted = true; michael@0: return record; michael@0: } michael@0: michael@0: let parent = PlacesUtils.bookmarks.getFolderIdForItem(placeId); michael@0: switch (PlacesUtils.bookmarks.getItemType(placeId)) { michael@0: case PlacesUtils.bookmarks.TYPE_BOOKMARK: michael@0: let bmkUri = PlacesUtils.bookmarks.getBookmarkURI(placeId).spec; michael@0: if (bmkUri.indexOf("place:") == 0) { michael@0: record = new BookmarkQuery(collection, id); michael@0: michael@0: // Get the actual tag name instead of the local itemId michael@0: let folder = bmkUri.match(/[:&]folder=(\d+)/); michael@0: try { michael@0: // There might not be the tag yet when creating on a new client michael@0: if (folder != null) { michael@0: folder = folder[1]; michael@0: record.folderName = PlacesUtils.bookmarks.getItemTitle(folder); michael@0: this._log.trace("query id: " + folder + " = " + record.folderName); michael@0: } michael@0: } michael@0: catch(ex) {} michael@0: michael@0: // Persist the Smart Bookmark anno, if found. michael@0: try { michael@0: let anno = PlacesUtils.annotations.getItemAnnotation(placeId, SMART_BOOKMARKS_ANNO); michael@0: if (anno != null) { michael@0: this._log.trace("query anno: " + SMART_BOOKMARKS_ANNO + michael@0: " = " + anno); michael@0: record.queryId = anno; michael@0: } michael@0: } michael@0: catch(ex) {} michael@0: } michael@0: else { michael@0: record = new Bookmark(collection, id); michael@0: } michael@0: record.title = PlacesUtils.bookmarks.getItemTitle(placeId); michael@0: michael@0: record.parentName = PlacesUtils.bookmarks.getItemTitle(parent); michael@0: record.bmkUri = bmkUri; michael@0: record.tags = this._getTags(record.bmkUri); michael@0: record.keyword = PlacesUtils.bookmarks.getKeywordForBookmark(placeId); michael@0: record.description = this._getDescription(placeId); michael@0: record.loadInSidebar = this._isLoadInSidebar(placeId); michael@0: break; michael@0: michael@0: case PlacesUtils.bookmarks.TYPE_FOLDER: michael@0: if (PlacesUtils.annotations michael@0: .itemHasAnnotation(placeId, PlacesUtils.LMANNO_FEEDURI)) { michael@0: record = new Livemark(collection, id); michael@0: let as = PlacesUtils.annotations; michael@0: record.feedUri = as.getItemAnnotation(placeId, PlacesUtils.LMANNO_FEEDURI); michael@0: try { michael@0: record.siteUri = as.getItemAnnotation(placeId, PlacesUtils.LMANNO_SITEURI); michael@0: } catch (ex) {} michael@0: } else { michael@0: record = new BookmarkFolder(collection, id); michael@0: } michael@0: michael@0: if (parent > 0) michael@0: record.parentName = PlacesUtils.bookmarks.getItemTitle(parent); michael@0: record.title = PlacesUtils.bookmarks.getItemTitle(placeId); michael@0: record.description = this._getDescription(placeId); michael@0: record.children = this._getChildGUIDsForId(placeId); michael@0: break; michael@0: michael@0: case PlacesUtils.bookmarks.TYPE_SEPARATOR: michael@0: record = new BookmarkSeparator(collection, id); michael@0: if (parent > 0) michael@0: record.parentName = PlacesUtils.bookmarks.getItemTitle(parent); michael@0: // Create a positioning identifier for the separator, used by _mapDupe michael@0: record.pos = PlacesUtils.bookmarks.getItemIndex(placeId); michael@0: break; michael@0: michael@0: default: michael@0: record = new PlacesItem(collection, id); michael@0: this._log.warn("Unknown item type, cannot serialize: " + michael@0: PlacesUtils.bookmarks.getItemType(placeId)); michael@0: } michael@0: michael@0: record.parentid = this.GUIDForId(parent); michael@0: record.sortindex = this._calculateIndex(record); michael@0: michael@0: return record; michael@0: }, michael@0: michael@0: _stmts: {}, michael@0: _getStmt: function(query) { michael@0: if (query in this._stmts) { michael@0: return this._stmts[query]; michael@0: } michael@0: michael@0: this._log.trace("Creating SQL statement: " + query); michael@0: let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) michael@0: .DBConnection; michael@0: return this._stmts[query] = db.createAsyncStatement(query); michael@0: }, michael@0: michael@0: get _frecencyStm() { michael@0: return this._getStmt( michael@0: "SELECT frecency " + michael@0: "FROM moz_places " + michael@0: "WHERE url = :url " + michael@0: "LIMIT 1"); michael@0: }, michael@0: _frecencyCols: ["frecency"], michael@0: michael@0: get _setGUIDStm() { michael@0: return this._getStmt( michael@0: "UPDATE moz_bookmarks " + michael@0: "SET guid = :guid " + michael@0: "WHERE id = :item_id"); michael@0: }, michael@0: michael@0: // Some helper functions to handle GUIDs michael@0: _setGUID: function _setGUID(id, guid) { michael@0: if (!guid) michael@0: guid = Utils.makeGUID(); michael@0: michael@0: let stmt = this._setGUIDStm; michael@0: stmt.params.guid = guid; michael@0: stmt.params.item_id = id; michael@0: Async.querySpinningly(stmt); michael@0: return guid; michael@0: }, michael@0: michael@0: get _guidForIdStm() { michael@0: return this._getStmt( michael@0: "SELECT guid " + michael@0: "FROM moz_bookmarks " + michael@0: "WHERE id = :item_id"); michael@0: }, michael@0: _guidForIdCols: ["guid"], michael@0: michael@0: GUIDForId: function GUIDForId(id) { michael@0: let special = kSpecialIds.specialGUIDForId(id); michael@0: if (special) michael@0: return special; michael@0: michael@0: let stmt = this._guidForIdStm; michael@0: stmt.params.item_id = id; michael@0: michael@0: // Use the existing GUID if it exists michael@0: let result = Async.querySpinningly(stmt, this._guidForIdCols)[0]; michael@0: if (result && result.guid) michael@0: return result.guid; michael@0: michael@0: // Give the uri a GUID if it doesn't have one michael@0: return this._setGUID(id); michael@0: }, michael@0: michael@0: get _idForGUIDStm() { michael@0: return this._getStmt( michael@0: "SELECT id AS item_id " + michael@0: "FROM moz_bookmarks " + michael@0: "WHERE guid = :guid"); michael@0: }, michael@0: _idForGUIDCols: ["item_id"], michael@0: michael@0: // noCreate is provided as an optional argument to prevent the creation of michael@0: // non-existent special records, such as "mobile". michael@0: idForGUID: function idForGUID(guid, noCreate) { michael@0: if (kSpecialIds.isSpecialGUID(guid)) michael@0: return kSpecialIds.specialIdForGUID(guid, !noCreate); michael@0: michael@0: let stmt = this._idForGUIDStm; michael@0: // guid might be a String object rather than a string. michael@0: stmt.params.guid = guid.toString(); michael@0: michael@0: let results = Async.querySpinningly(stmt, this._idForGUIDCols); michael@0: this._log.trace("Number of rows matching GUID " + guid + ": " michael@0: + results.length); michael@0: michael@0: // Here's the one we care about: the first. michael@0: let result = results[0]; michael@0: michael@0: if (!result) michael@0: return -1; michael@0: michael@0: return result.item_id; michael@0: }, michael@0: michael@0: _calculateIndex: function _calculateIndex(record) { michael@0: // Ensure folders have a very high sort index so they're not synced last. michael@0: if (record.type == "folder") michael@0: return FOLDER_SORTINDEX; michael@0: michael@0: // For anything directly under the toolbar, give it a boost of more than an michael@0: // unvisited bookmark michael@0: let index = 0; michael@0: if (record.parentid == "toolbar") michael@0: index += 150; michael@0: michael@0: // Add in the bookmark's frecency if we have something. michael@0: if (record.bmkUri != null) { michael@0: this._frecencyStm.params.url = record.bmkUri; michael@0: let result = Async.querySpinningly(this._frecencyStm, this._frecencyCols); michael@0: if (result.length) michael@0: index += result[0].frecency; michael@0: } michael@0: michael@0: return index; michael@0: }, michael@0: michael@0: _getChildren: function BStore_getChildren(guid, items) { michael@0: let node = guid; // the recursion case michael@0: if (typeof(node) == "string") { // callers will give us the guid as the first arg michael@0: let nodeID = this.idForGUID(guid, true); michael@0: if (!nodeID) { michael@0: this._log.debug("No node for GUID " + guid + "; returning no children."); michael@0: return items; michael@0: } michael@0: node = this._getNode(nodeID); michael@0: } michael@0: michael@0: if (node.type == node.RESULT_TYPE_FOLDER) { michael@0: node.QueryInterface(Ci.nsINavHistoryQueryResultNode); michael@0: node.containerOpen = true; michael@0: try { michael@0: // Remember all the children GUIDs and recursively get more michael@0: for (let i = 0; i < node.childCount; i++) { michael@0: let child = node.getChild(i); michael@0: items[this.GUIDForId(child.itemId)] = true; michael@0: this._getChildren(child, items); michael@0: } michael@0: } michael@0: finally { michael@0: node.containerOpen = false; michael@0: } michael@0: } michael@0: michael@0: return items; michael@0: }, michael@0: michael@0: /** michael@0: * Associates the URI of the item with the provided ID with the michael@0: * provided array of tags. michael@0: * If the provided ID does not identify an item with a URI, michael@0: * returns immediately. michael@0: */ michael@0: _tagID: function _tagID(itemID, tags) { michael@0: if (!itemID || !tags) { michael@0: return; michael@0: } michael@0: michael@0: try { michael@0: let u = PlacesUtils.bookmarks.getBookmarkURI(itemID); michael@0: this._tagURI(u, tags); michael@0: } catch (e) { michael@0: this._log.warn("Got exception fetching URI for " + itemID + ": not tagging. " + michael@0: Utils.exceptionStr(e)); michael@0: michael@0: // I guess it doesn't have a URI. Don't try to tag it. michael@0: return; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Associate the provided URI with the provided array of tags. michael@0: * If the provided URI is falsy, returns immediately. michael@0: */ michael@0: _tagURI: function _tagURI(bookmarkURI, tags) { michael@0: if (!bookmarkURI || !tags) { michael@0: return; michael@0: } michael@0: michael@0: // Filter out any null/undefined/empty tags. michael@0: tags = tags.filter(function(t) t); michael@0: michael@0: // Temporarily tag a dummy URI to preserve tag ids when untagging. michael@0: let dummyURI = Utils.makeURI("about:weave#BStore_tagURI"); michael@0: PlacesUtils.tagging.tagURI(dummyURI, tags); michael@0: PlacesUtils.tagging.untagURI(bookmarkURI, null); michael@0: PlacesUtils.tagging.tagURI(bookmarkURI, tags); michael@0: PlacesUtils.tagging.untagURI(dummyURI, null); michael@0: }, michael@0: michael@0: getAllIDs: function BStore_getAllIDs() { michael@0: let items = {"menu": true, michael@0: "toolbar": true}; michael@0: for each (let guid in kSpecialIds.guids) { michael@0: if (guid != "places" && guid != "tags") michael@0: this._getChildren(guid, items); michael@0: } michael@0: return items; michael@0: }, michael@0: michael@0: wipe: function BStore_wipe() { michael@0: let cb = Async.makeSpinningCallback(); michael@0: Task.spawn(function() { michael@0: // Save a backup before clearing out all bookmarks. michael@0: yield PlacesBackups.create(null, true); michael@0: for each (let guid in kSpecialIds.guids) michael@0: if (guid != "places") { michael@0: let id = kSpecialIds.specialIdForGUID(guid); michael@0: if (id) michael@0: PlacesUtils.bookmarks.removeFolderChildren(id); michael@0: } michael@0: cb(); michael@0: }); michael@0: cb.wait(); michael@0: } michael@0: }; michael@0: michael@0: function BookmarksTracker(name, engine) { michael@0: Tracker.call(this, name, engine); michael@0: michael@0: Svc.Obs.add("places-shutdown", this); michael@0: } michael@0: BookmarksTracker.prototype = { michael@0: __proto__: Tracker.prototype, michael@0: michael@0: startTracking: function() { michael@0: PlacesUtils.bookmarks.addObserver(this, true); michael@0: Svc.Obs.add("bookmarks-restore-begin", this); michael@0: Svc.Obs.add("bookmarks-restore-success", this); michael@0: Svc.Obs.add("bookmarks-restore-failed", this); michael@0: }, michael@0: michael@0: stopTracking: function() { michael@0: PlacesUtils.bookmarks.removeObserver(this); michael@0: Svc.Obs.remove("bookmarks-restore-begin", this); michael@0: Svc.Obs.remove("bookmarks-restore-success", this); michael@0: Svc.Obs.remove("bookmarks-restore-failed", this); michael@0: }, michael@0: michael@0: observe: function observe(subject, topic, data) { michael@0: Tracker.prototype.observe.call(this, subject, topic, data); michael@0: michael@0: switch (topic) { michael@0: case "bookmarks-restore-begin": michael@0: this._log.debug("Ignoring changes from importing bookmarks."); michael@0: this.ignoreAll = true; michael@0: break; michael@0: case "bookmarks-restore-success": michael@0: this._log.debug("Tracking all items on successful import."); michael@0: this.ignoreAll = false; michael@0: michael@0: this._log.debug("Restore succeeded: wiping server and other clients."); michael@0: this.engine.service.resetClient([this.name]); michael@0: this.engine.service.wipeServer([this.name]); michael@0: this.engine.service.clientsEngine.sendCommand("wipeEngine", [this.name]); michael@0: break; michael@0: case "bookmarks-restore-failed": michael@0: this._log.debug("Tracking all items on failed import."); michael@0: this.ignoreAll = false; michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsINavBookmarkObserver, michael@0: Ci.nsINavBookmarkObserver_MOZILLA_1_9_1_ADDITIONS, michael@0: Ci.nsISupportsWeakReference michael@0: ]), michael@0: michael@0: /** michael@0: * Add a bookmark GUID to be uploaded and bump up the sync score. michael@0: * michael@0: * @param itemGuid michael@0: * GUID of the bookmark to upload. michael@0: */ michael@0: _add: function BMT__add(itemId, guid) { michael@0: guid = kSpecialIds.specialGUIDForId(itemId) || guid; michael@0: if (this.addChangedID(guid)) michael@0: this._upScore(); michael@0: }, michael@0: michael@0: /* Every add/remove/change will trigger a sync for MULTI_DEVICE. */ michael@0: _upScore: function BMT__upScore() { michael@0: this.score += SCORE_INCREMENT_XLARGE; michael@0: }, michael@0: michael@0: /** michael@0: * Determine if a change should be ignored. michael@0: * michael@0: * @param itemId michael@0: * Item under consideration to ignore michael@0: * @param folder (optional) michael@0: * Folder of the item being changed michael@0: */ michael@0: _ignore: function BMT__ignore(itemId, folder, guid) { michael@0: // Ignore unconditionally if the engine tells us to. michael@0: if (this.ignoreAll) michael@0: return true; michael@0: michael@0: // Get the folder id if we weren't given one. michael@0: if (folder == null) { michael@0: try { michael@0: folder = PlacesUtils.bookmarks.getFolderIdForItem(itemId); michael@0: } catch (ex) { michael@0: this._log.debug("getFolderIdForItem(" + itemId + michael@0: ") threw; calling _ensureMobileQuery."); michael@0: // I'm guessing that gFIFI can throw, and perhaps that's why michael@0: // _ensureMobileQuery is here at all. Try not to call it. michael@0: this._ensureMobileQuery(); michael@0: folder = PlacesUtils.bookmarks.getFolderIdForItem(itemId); michael@0: } michael@0: } michael@0: michael@0: // Ignore changes to tags (folders under the tags folder). michael@0: let tags = kSpecialIds.tags; michael@0: if (folder == tags) michael@0: return true; michael@0: michael@0: // Ignore tag items (the actual instance of a tag for a bookmark). michael@0: if (PlacesUtils.bookmarks.getFolderIdForItem(folder) == tags) michael@0: return true; michael@0: michael@0: // Make sure to remove items that have the exclude annotation. michael@0: if (PlacesUtils.annotations.itemHasAnnotation(itemId, EXCLUDEBACKUP_ANNO)) { michael@0: this.removeChangedID(guid); michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: }, michael@0: michael@0: onItemAdded: function BMT_onItemAdded(itemId, folder, index, michael@0: itemType, uri, title, dateAdded, michael@0: guid, parentGuid) { michael@0: if (this._ignore(itemId, folder, guid)) michael@0: return; michael@0: michael@0: this._log.trace("onItemAdded: " + itemId); michael@0: this._add(itemId, guid); michael@0: this._add(folder, parentGuid); michael@0: }, michael@0: michael@0: onItemRemoved: function (itemId, parentId, index, type, uri, michael@0: guid, parentGuid) { michael@0: if (this._ignore(itemId, parentId, guid)) { michael@0: return; michael@0: } michael@0: michael@0: this._log.trace("onItemRemoved: " + itemId); michael@0: this._add(itemId, guid); michael@0: this._add(parentId, parentGuid); michael@0: }, michael@0: michael@0: _ensureMobileQuery: function _ensureMobileQuery() { michael@0: let find = function (val) michael@0: PlacesUtils.annotations.getItemsWithAnnotation(ORGANIZERQUERY_ANNO, {}).filter( michael@0: function (id) PlacesUtils.annotations.getItemAnnotation(id, ORGANIZERQUERY_ANNO) == val michael@0: ); michael@0: michael@0: // Don't continue if the Library isn't ready michael@0: let all = find(ALLBOOKMARKS_ANNO); michael@0: if (all.length == 0) michael@0: return; michael@0: michael@0: // Disable handling of notifications while changing the mobile query michael@0: this.ignoreAll = true; michael@0: michael@0: let mobile = find(MOBILE_ANNO); michael@0: let queryURI = Utils.makeURI("place:folder=" + kSpecialIds.mobile); michael@0: let title = Str.sync.get("mobile.label"); michael@0: michael@0: // Don't add OR remove the mobile bookmarks if there's nothing. michael@0: if (PlacesUtils.bookmarks.getIdForItemAt(kSpecialIds.mobile, 0) == -1) { michael@0: if (mobile.length != 0) michael@0: PlacesUtils.bookmarks.removeItem(mobile[0]); michael@0: } michael@0: // Add the mobile bookmarks query if it doesn't exist michael@0: else if (mobile.length == 0) { michael@0: let query = PlacesUtils.bookmarks.insertBookmark(all[0], queryURI, -1, title); michael@0: PlacesUtils.annotations.setItemAnnotation(query, ORGANIZERQUERY_ANNO, MOBILE_ANNO, 0, michael@0: PlacesUtils.annotations.EXPIRE_NEVER); michael@0: PlacesUtils.annotations.setItemAnnotation(query, EXCLUDEBACKUP_ANNO, 1, 0, michael@0: PlacesUtils.annotations.EXPIRE_NEVER); michael@0: } michael@0: // Make sure the existing title is correct michael@0: else if (PlacesUtils.bookmarks.getItemTitle(mobile[0]) != title) { michael@0: PlacesUtils.bookmarks.setItemTitle(mobile[0], title); michael@0: } michael@0: michael@0: this.ignoreAll = false; michael@0: }, michael@0: michael@0: // This method is oddly structured, but the idea is to return as quickly as michael@0: // possible -- this handler gets called *every time* a bookmark changes, for michael@0: // *each change*. michael@0: onItemChanged: function BMT_onItemChanged(itemId, property, isAnno, value, michael@0: lastModified, itemType, parentId, michael@0: guid, parentGuid) { michael@0: // Quicker checks first. michael@0: if (this.ignoreAll) michael@0: return; michael@0: michael@0: if (isAnno && (ANNOS_TO_TRACK.indexOf(property) == -1)) michael@0: // Ignore annotations except for the ones that we sync. michael@0: return; michael@0: michael@0: // Ignore favicon changes to avoid unnecessary churn. michael@0: if (property == "favicon") michael@0: return; michael@0: michael@0: if (this._ignore(itemId, parentId, guid)) michael@0: return; michael@0: michael@0: this._log.trace("onItemChanged: " + itemId + michael@0: (", " + property + (isAnno? " (anno)" : "")) + michael@0: (value ? (" = \"" + value + "\"") : "")); michael@0: this._add(itemId, guid); michael@0: }, michael@0: michael@0: onItemMoved: function BMT_onItemMoved(itemId, oldParent, oldIndex, michael@0: newParent, newIndex, itemType, michael@0: guid, oldParentGuid, newParentGuid) { michael@0: if (this._ignore(itemId, newParent, guid)) michael@0: return; michael@0: michael@0: this._log.trace("onItemMoved: " + itemId); michael@0: this._add(oldParent, oldParentGuid); michael@0: if (oldParent != newParent) { michael@0: this._add(itemId, guid); michael@0: this._add(newParent, newParentGuid); michael@0: } michael@0: michael@0: // Remove any position annotations now that the user moved the item michael@0: PlacesUtils.annotations.removeItemAnnotation(itemId, PARENT_ANNO); michael@0: }, michael@0: michael@0: onBeginUpdateBatch: function () {}, michael@0: onEndUpdateBatch: function () {}, michael@0: onItemVisited: function () {} michael@0: };