services/sync/modules/engines/bookmarks.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 this.EXPORTED_SYMBOLS = ['BookmarksEngine', "PlacesItem", "Bookmark",
     6                          "BookmarkFolder", "BookmarkQuery",
     7                          "Livemark", "BookmarkSeparator"];
     9 const Cc = Components.classes;
    10 const Ci = Components.interfaces;
    11 const Cu = Components.utils;
    13 Cu.import("resource://gre/modules/PlacesUtils.jsm");
    14 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    15 Cu.import("resource://services-common/async.js");
    16 Cu.import("resource://services-sync/constants.js");
    17 Cu.import("resource://services-sync/engines.js");
    18 Cu.import("resource://services-sync/record.js");
    19 Cu.import("resource://services-sync/util.js");
    20 Cu.import("resource://gre/modules/Task.jsm");
    21 Cu.import("resource://gre/modules/PlacesBackups.jsm");
    23 const ALLBOOKMARKS_ANNO    = "AllBookmarks";
    24 const DESCRIPTION_ANNO     = "bookmarkProperties/description";
    25 const SIDEBAR_ANNO         = "bookmarkProperties/loadInSidebar";
    26 const MOBILEROOT_ANNO      = "mobile/bookmarksRoot";
    27 const MOBILE_ANNO          = "MobileBookmarks";
    28 const EXCLUDEBACKUP_ANNO   = "places/excludeFromBackup";
    29 const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
    30 const PARENT_ANNO          = "sync/parent";
    31 const ORGANIZERQUERY_ANNO  = "PlacesOrganizer/OrganizerQuery";
    32 const ANNOS_TO_TRACK = [DESCRIPTION_ANNO, SIDEBAR_ANNO,
    33                         PlacesUtils.LMANNO_FEEDURI, PlacesUtils.LMANNO_SITEURI];
    35 const SERVICE_NOT_SUPPORTED = "Service not supported on this platform";
    36 const FOLDER_SORTINDEX = 1000000;
    38 this.PlacesItem = function PlacesItem(collection, id, type) {
    39   CryptoWrapper.call(this, collection, id);
    40   this.type = type || "item";
    41 }
    42 PlacesItem.prototype = {
    43   decrypt: function PlacesItem_decrypt(keyBundle) {
    44     // Do the normal CryptoWrapper decrypt, but change types before returning
    45     let clear = CryptoWrapper.prototype.decrypt.call(this, keyBundle);
    47     // Convert the abstract places item to the actual object type
    48     if (!this.deleted)
    49       this.__proto__ = this.getTypeObject(this.type).prototype;
    51     return clear;
    52   },
    54   getTypeObject: function PlacesItem_getTypeObject(type) {
    55     switch (type) {
    56       case "bookmark":
    57       case "microsummary":
    58         return Bookmark;
    59       case "query":
    60         return BookmarkQuery;
    61       case "folder":
    62         return BookmarkFolder;
    63       case "livemark":
    64         return Livemark;
    65       case "separator":
    66         return BookmarkSeparator;
    67       case "item":
    68         return PlacesItem;
    69     }
    70     throw "Unknown places item object type: " + type;
    71   },
    73   __proto__: CryptoWrapper.prototype,
    74   _logName: "Sync.Record.PlacesItem",
    75 };
    77 Utils.deferGetSet(PlacesItem,
    78                   "cleartext",
    79                   ["hasDupe", "parentid", "parentName", "type"]);
    81 this.Bookmark = function Bookmark(collection, id, type) {
    82   PlacesItem.call(this, collection, id, type || "bookmark");
    83 }
    84 Bookmark.prototype = {
    85   __proto__: PlacesItem.prototype,
    86   _logName: "Sync.Record.Bookmark",
    87 };
    89 Utils.deferGetSet(Bookmark,
    90                   "cleartext",
    91                   ["title", "bmkUri", "description",
    92                    "loadInSidebar", "tags", "keyword"]);
    94 this.BookmarkQuery = function BookmarkQuery(collection, id) {
    95   Bookmark.call(this, collection, id, "query");
    96 }
    97 BookmarkQuery.prototype = {
    98   __proto__: Bookmark.prototype,
    99   _logName: "Sync.Record.BookmarkQuery",
   100 };
   102 Utils.deferGetSet(BookmarkQuery,
   103                   "cleartext",
   104                   ["folderName", "queryId"]);
   106 this.BookmarkFolder = function BookmarkFolder(collection, id, type) {
   107   PlacesItem.call(this, collection, id, type || "folder");
   108 }
   109 BookmarkFolder.prototype = {
   110   __proto__: PlacesItem.prototype,
   111   _logName: "Sync.Record.Folder",
   112 };
   114 Utils.deferGetSet(BookmarkFolder, "cleartext", ["description", "title",
   115                                                 "children"]);
   117 this.Livemark = function Livemark(collection, id) {
   118   BookmarkFolder.call(this, collection, id, "livemark");
   119 }
   120 Livemark.prototype = {
   121   __proto__: BookmarkFolder.prototype,
   122   _logName: "Sync.Record.Livemark",
   123 };
   125 Utils.deferGetSet(Livemark, "cleartext", ["siteUri", "feedUri"]);
   127 this.BookmarkSeparator = function BookmarkSeparator(collection, id) {
   128   PlacesItem.call(this, collection, id, "separator");
   129 }
   130 BookmarkSeparator.prototype = {
   131   __proto__: PlacesItem.prototype,
   132   _logName: "Sync.Record.Separator",
   133 };
   135 Utils.deferGetSet(BookmarkSeparator, "cleartext", "pos");
   138 let kSpecialIds = {
   140   // Special IDs. Note that mobile can attempt to create a record on
   141   // dereference; special accessors are provided to prevent recursion within
   142   // observers.
   143   guids: ["menu", "places", "tags", "toolbar", "unfiled", "mobile"],
   145   // Create the special mobile folder to store mobile bookmarks.
   146   createMobileRoot: function createMobileRoot() {
   147     let root = PlacesUtils.placesRootId;
   148     let mRoot = PlacesUtils.bookmarks.createFolder(root, "mobile", -1);
   149     PlacesUtils.annotations.setItemAnnotation(
   150       mRoot, MOBILEROOT_ANNO, 1, 0, PlacesUtils.annotations.EXPIRE_NEVER);
   151     PlacesUtils.annotations.setItemAnnotation(
   152       mRoot, EXCLUDEBACKUP_ANNO, 1, 0, PlacesUtils.annotations.EXPIRE_NEVER);
   153     return mRoot;
   154   },
   156   findMobileRoot: function findMobileRoot(create) {
   157     // Use the (one) mobile root if it already exists.
   158     let root = PlacesUtils.annotations.getItemsWithAnnotation(MOBILEROOT_ANNO, {});
   159     if (root.length != 0)
   160       return root[0];
   162     if (create)
   163       return this.createMobileRoot();
   165     return null;
   166   },
   168   // Accessors for IDs.
   169   isSpecialGUID: function isSpecialGUID(g) {
   170     return this.guids.indexOf(g) != -1;
   171   },
   173   specialIdForGUID: function specialIdForGUID(guid, create) {
   174     if (guid == "mobile") {
   175       return this.findMobileRoot(create);
   176     }
   177     return this[guid];
   178   },
   180   // Don't bother creating mobile: if it doesn't exist, this ID can't be it!
   181   specialGUIDForId: function specialGUIDForId(id) {
   182     for each (let guid in this.guids)
   183       if (this.specialIdForGUID(guid, false) == id)
   184         return guid;
   185     return null;
   186   },
   188   get menu()    PlacesUtils.bookmarksMenuFolderId,
   189   get places()  PlacesUtils.placesRootId,
   190   get tags()    PlacesUtils.tagsFolderId,
   191   get toolbar() PlacesUtils.toolbarFolderId,
   192   get unfiled() PlacesUtils.unfiledBookmarksFolderId,
   193   get mobile()  this.findMobileRoot(true),
   194 };
   196 this.BookmarksEngine = function BookmarksEngine(service) {
   197   SyncEngine.call(this, "Bookmarks", service);
   198 }
   199 BookmarksEngine.prototype = {
   200   __proto__: SyncEngine.prototype,
   201   _recordObj: PlacesItem,
   202   _storeObj: BookmarksStore,
   203   _trackerObj: BookmarksTracker,
   204   version: 2,
   206   _sync: function _sync() {
   207     let engine = this;
   208     let batchEx = null;
   210     // Try running sync in batch mode
   211     PlacesUtils.bookmarks.runInBatchMode({
   212       runBatched: function wrappedSync() {
   213         try {
   214           SyncEngine.prototype._sync.call(engine);
   215         }
   216         catch(ex) {
   217           batchEx = ex;
   218         }
   219       }
   220     }, null);
   222     // Expose the exception if something inside the batch failed
   223     if (batchEx != null) {
   224       throw batchEx;
   225     }
   226   },
   228   _guidMapFailed: false,
   229   _buildGUIDMap: function _buildGUIDMap() {
   230     let guidMap = {};
   231     for (let guid in this._store.getAllIDs()) {
   232       // Figure out with which key to store the mapping.
   233       let key;
   234       let id = this._store.idForGUID(guid);
   235       switch (PlacesUtils.bookmarks.getItemType(id)) {
   236         case PlacesUtils.bookmarks.TYPE_BOOKMARK:
   238           // Smart bookmarks map to their annotation value.
   239           let queryId;
   240           try {
   241             queryId = PlacesUtils.annotations.getItemAnnotation(
   242               id, SMART_BOOKMARKS_ANNO);
   243           } catch(ex) {}
   245           if (queryId)
   246             key = "q" + queryId;
   247           else
   248             key = "b" + PlacesUtils.bookmarks.getBookmarkURI(id).spec + ":" +
   249                   PlacesUtils.bookmarks.getItemTitle(id);
   250           break;
   251         case PlacesUtils.bookmarks.TYPE_FOLDER:
   252           key = "f" + PlacesUtils.bookmarks.getItemTitle(id);
   253           break;
   254         case PlacesUtils.bookmarks.TYPE_SEPARATOR:
   255           key = "s" + PlacesUtils.bookmarks.getItemIndex(id);
   256           break;
   257         default:
   258           continue;
   259       }
   261       // The mapping is on a per parent-folder-name basis.
   262       let parent = PlacesUtils.bookmarks.getFolderIdForItem(id);
   263       if (parent <= 0)
   264         continue;
   266       let parentName = PlacesUtils.bookmarks.getItemTitle(parent);
   267       if (guidMap[parentName] == null)
   268         guidMap[parentName] = {};
   270       // If the entry already exists, remember that there are explicit dupes.
   271       let entry = new String(guid);
   272       entry.hasDupe = guidMap[parentName][key] != null;
   274       // Remember this item's GUID for its parent-name/key pair.
   275       guidMap[parentName][key] = entry;
   276       this._log.trace("Mapped: " + [parentName, key, entry, entry.hasDupe]);
   277     }
   279     return guidMap;
   280   },
   282   // Helper function to get a dupe GUID for an item.
   283   _mapDupe: function _mapDupe(item) {
   284     // Figure out if we have something to key with.
   285     let key;
   286     let altKey;
   287     switch (item.type) {
   288       case "query":
   289         // Prior to Bug 610501, records didn't carry their Smart Bookmark
   290         // anno, so we won't be able to dupe them correctly. This altKey
   291         // hack should get them to dupe correctly.
   292         if (item.queryId) {
   293           key = "q" + item.queryId;
   294           altKey = "b" + item.bmkUri + ":" + item.title;
   295           break;
   296         }
   297         // No queryID? Fall through to the regular bookmark case.
   298       case "bookmark":
   299       case "microsummary":
   300         key = "b" + item.bmkUri + ":" + item.title;
   301         break;
   302       case "folder":
   303       case "livemark":
   304         key = "f" + item.title;
   305         break;
   306       case "separator":
   307         key = "s" + item.pos;
   308         break;
   309       default:
   310         return;
   311     }
   313     // Figure out if we have a map to use!
   314     // This will throw in some circumstances. That's fine.
   315     let guidMap = this._guidMap;
   317     // Give the GUID if we have the matching pair.
   318     this._log.trace("Finding mapping: " + item.parentName + ", " + key);
   319     let parent = guidMap[item.parentName];
   321     if (!parent) {
   322       this._log.trace("No parent => no dupe.");
   323       return undefined;
   324     }
   326     let dupe = parent[key];
   328     if (dupe) {
   329       this._log.trace("Mapped dupe: " + dupe);
   330       return dupe;
   331     }
   333     if (altKey) {
   334       dupe = parent[altKey];
   335       if (dupe) {
   336         this._log.trace("Mapped dupe using altKey " + altKey + ": " + dupe);
   337         return dupe;
   338       }
   339     }
   341     this._log.trace("No dupe found for key " + key + "/" + altKey + ".");
   342     return undefined;
   343   },
   345   _syncStartup: function _syncStart() {
   346     SyncEngine.prototype._syncStartup.call(this);
   348     let cb = Async.makeSpinningCallback();
   349     Task.spawn(function() {
   350       // For first-syncs, make a backup for the user to restore
   351       if (this.lastSync == 0) {
   352         this._log.debug("Bookmarks backup starting.");
   353         yield PlacesBackups.create(null, true);
   354         this._log.debug("Bookmarks backup done.");
   355       }
   356     }.bind(this)).then(
   357       cb, ex => {
   358         // Failure to create a backup is somewhat bad, but probably not bad
   359         // enough to prevent syncing of bookmarks - so just log the error and
   360         // continue.
   361         this._log.warn("Got exception \"" + Utils.exceptionStr(ex) +
   362                        "\" backing up bookmarks, but continuing with sync.");
   363         cb();
   364       }
   365     );
   367     cb.wait();
   369     this.__defineGetter__("_guidMap", function() {
   370       // Create a mapping of folder titles and separator positions to GUID.
   371       // We do this lazily so that we don't do any work unless we reconcile
   372       // incoming items.
   373       let guidMap;
   374       try {
   375         guidMap = this._buildGUIDMap();
   376       } catch (ex) {
   377         this._log.warn("Got exception \"" + Utils.exceptionStr(ex) +
   378                        "\" building GUID map." +
   379                        " Skipping all other incoming items.");
   380         throw {code: Engine.prototype.eEngineAbortApplyIncoming,
   381                cause: ex};
   382       }
   383       delete this._guidMap;
   384       return this._guidMap = guidMap;
   385     });
   387     this._store._childrenToOrder = {};
   388   },
   390   _processIncoming: function (newitems) {
   391     try {
   392       SyncEngine.prototype._processIncoming.call(this, newitems);
   393     } finally {
   394       // Reorder children.
   395       this._tracker.ignoreAll = true;
   396       this._store._orderChildren();
   397       this._tracker.ignoreAll = false;
   398       delete this._store._childrenToOrder;
   399     }
   400   },
   402   _syncFinish: function _syncFinish() {
   403     SyncEngine.prototype._syncFinish.call(this);
   404     this._tracker._ensureMobileQuery();
   405   },
   407   _syncCleanup: function _syncCleanup() {
   408     SyncEngine.prototype._syncCleanup.call(this);
   409     delete this._guidMap;
   410   },
   412   _createRecord: function _createRecord(id) {
   413     // Create the record as usual, but mark it as having dupes if necessary.
   414     let record = SyncEngine.prototype._createRecord.call(this, id);
   415     let entry = this._mapDupe(record);
   416     if (entry != null && entry.hasDupe) {
   417       record.hasDupe = true;
   418     }
   419     return record;
   420   },
   422   _findDupe: function _findDupe(item) {
   423     this._log.trace("Finding dupe for " + item.id +
   424                     " (already duped: " + item.hasDupe + ").");
   426     // Don't bother finding a dupe if the incoming item has duplicates.
   427     if (item.hasDupe) {
   428       this._log.trace(item.id + " already a dupe: not finding one.");
   429       return;
   430     }
   431     let mapped = this._mapDupe(item);
   432     this._log.debug(item.id + " mapped to " + mapped);
   433     return mapped;
   434   }
   435 };
   437 function BookmarksStore(name, engine) {
   438   Store.call(this, name, engine);
   440   // Explicitly nullify our references to our cached services so we don't leak
   441   Svc.Obs.add("places-shutdown", function() {
   442     for each (let [query, stmt] in Iterator(this._stmts)) {
   443       stmt.finalize();
   444     }
   445     this._stmts = {};
   446   }, this);
   447 }
   448 BookmarksStore.prototype = {
   449   __proto__: Store.prototype,
   451   itemExists: function BStore_itemExists(id) {
   452     return this.idForGUID(id, true) > 0;
   453   },
   455   /*
   456    * If the record is a tag query, rewrite it to refer to the local tag ID.
   457    * 
   458    * Otherwise, just return.
   459    */
   460   preprocessTagQuery: function preprocessTagQuery(record) {
   461     if (record.type != "query" ||
   462         record.bmkUri == null ||
   463         !record.folderName)
   464       return;
   466     // Yes, this works without chopping off the "place:" prefix.
   467     let uri           = record.bmkUri
   468     let queriesRef    = {};
   469     let queryCountRef = {};
   470     let optionsRef    = {};
   471     PlacesUtils.history.queryStringToQueries(uri, queriesRef, queryCountRef,
   472                                              optionsRef);
   474     // We only process tag URIs.
   475     if (optionsRef.value.resultType != optionsRef.value.RESULTS_AS_TAG_CONTENTS)
   476       return;
   478     // Tag something to ensure that the tag exists.
   479     let tag = record.folderName;
   480     let dummyURI = Utils.makeURI("about:weave#BStore_preprocess");
   481     PlacesUtils.tagging.tagURI(dummyURI, [tag]);
   483     // Look for the id of the tag, which might just have been added.
   484     let tags = this._getNode(PlacesUtils.tagsFolderId);
   485     if (!(tags instanceof Ci.nsINavHistoryQueryResultNode)) {
   486       this._log.debug("tags isn't an nsINavHistoryQueryResultNode; aborting.");
   487       return;
   488     }
   490     tags.containerOpen = true;
   491     try {
   492       for (let i = 0; i < tags.childCount; i++) {
   493         let child = tags.getChild(i);
   494         if (child.title == tag) {
   495           // Found the tag, so fix up the query to use the right id.
   496           this._log.debug("Tag query folder: " + tag + " = " + child.itemId);
   498           this._log.trace("Replacing folders in: " + uri);
   499           for each (let q in queriesRef.value)
   500             q.setFolders([child.itemId], 1);
   502           record.bmkUri = PlacesUtils.history.queriesToQueryString(
   503             queriesRef.value, queryCountRef.value, optionsRef.value);
   504           return;
   505         }
   506       }
   507     }
   508     finally {
   509       tags.containerOpen = false;
   510     }
   511   },
   513   applyIncoming: function BStore_applyIncoming(record) {
   514     this._log.debug("Applying record " + record.id);
   515     let isSpecial = record.id in kSpecialIds;
   517     if (record.deleted) {
   518       if (isSpecial) {
   519         this._log.warn("Ignoring deletion for special record " + record.id);
   520         return;
   521       }
   523       // Don't bother with pre and post-processing for deletions.
   524       Store.prototype.applyIncoming.call(this, record);
   525       return;
   526     }
   528     // For special folders we're only interested in child ordering.
   529     if (isSpecial && record.children) {
   530       this._log.debug("Processing special node: " + record.id);
   531       // Reorder children later
   532       this._childrenToOrder[record.id] = record.children;
   533       return;
   534     }
   536     // Skip malformed records. (Bug 806460.)
   537     if (record.type == "query" &&
   538         !record.bmkUri) {
   539       this._log.warn("Skipping malformed query bookmark: " + record.id);
   540       return;
   541     }
   543     // Preprocess the record before doing the normal apply.
   544     this.preprocessTagQuery(record);
   546     // Figure out the local id of the parent GUID if available
   547     let parentGUID = record.parentid;
   548     if (!parentGUID) {
   549       throw "Record " + record.id + " has invalid parentid: " + parentGUID;
   550     }
   551     this._log.debug("Local parent is " + parentGUID);
   553     let parentId = this.idForGUID(parentGUID);
   554     if (parentId > 0) {
   555       // Save the parent id for modifying the bookmark later
   556       record._parent = parentId;
   557       record._orphan = false;
   558       this._log.debug("Record " + record.id + " is not an orphan.");
   559     } else {
   560       this._log.trace("Record " + record.id +
   561                       " is an orphan: could not find parent " + parentGUID);
   562       record._orphan = true;
   563     }
   565     // Do the normal processing of incoming records
   566     Store.prototype.applyIncoming.call(this, record);
   568     // Do some post-processing if we have an item
   569     let itemId = this.idForGUID(record.id);
   570     if (itemId > 0) {
   571       // Move any children that are looking for this folder as a parent
   572       if (record.type == "folder") {
   573         this._reparentOrphans(itemId);
   574         // Reorder children later
   575         if (record.children)
   576           this._childrenToOrder[record.id] = record.children;
   577       }
   579       // Create an annotation to remember that it needs reparenting.
   580       if (record._orphan) {
   581         PlacesUtils.annotations.setItemAnnotation(
   582           itemId, PARENT_ANNO, parentGUID, 0,
   583           PlacesUtils.annotations.EXPIRE_NEVER);
   584       }
   585     }
   586   },
   588   /**
   589    * Find all ids of items that have a given value for an annotation
   590    */
   591   _findAnnoItems: function BStore__findAnnoItems(anno, val) {
   592     return PlacesUtils.annotations.getItemsWithAnnotation(anno, {})
   593                       .filter(function(id) {
   594       return PlacesUtils.annotations.getItemAnnotation(id, anno) == val;
   595     });
   596   },
   598   /**
   599    * For the provided parent item, attach its children to it
   600    */
   601   _reparentOrphans: function _reparentOrphans(parentId) {
   602     // Find orphans and reunite with this folder parent
   603     let parentGUID = this.GUIDForId(parentId);
   604     let orphans = this._findAnnoItems(PARENT_ANNO, parentGUID);
   606     this._log.debug("Reparenting orphans " + orphans + " to " + parentId);
   607     orphans.forEach(function(orphan) {
   608       // Move the orphan to the parent and drop the missing parent annotation
   609       if (this._reparentItem(orphan, parentId)) {
   610         PlacesUtils.annotations.removeItemAnnotation(orphan, PARENT_ANNO);
   611       }
   612     }, this);
   613   },
   615   _reparentItem: function _reparentItem(itemId, parentId) {
   616     this._log.trace("Attempting to move item " + itemId + " to new parent " +
   617                     parentId);
   618     try {
   619       if (parentId > 0) {
   620         PlacesUtils.bookmarks.moveItem(itemId, parentId,
   621                                        PlacesUtils.bookmarks.DEFAULT_INDEX);
   622         return true;
   623       }
   624     } catch(ex) {
   625       this._log.debug("Failed to reparent item. " + Utils.exceptionStr(ex));
   626     }
   627     return false;
   628   },
   630   // Turn a record's nsINavBookmarksService constant and other attributes into
   631   // a granular type for comparison.
   632   _recordType: function _recordType(itemId) {
   633     let bms  = PlacesUtils.bookmarks;
   634     let type = bms.getItemType(itemId);
   636     switch (type) {
   637       case bms.TYPE_FOLDER:
   638         if (PlacesUtils.annotations
   639                        .itemHasAnnotation(itemId, PlacesUtils.LMANNO_FEEDURI)) {
   640           return "livemark";
   641         }
   642         return "folder";
   644       case bms.TYPE_BOOKMARK:
   645         let bmkUri = bms.getBookmarkURI(itemId).spec;
   646         if (bmkUri.indexOf("place:") == 0) {
   647           return "query";
   648         }
   649         return "bookmark";
   651       case bms.TYPE_SEPARATOR:
   652         return "separator";
   654       default:
   655         return null;
   656     }
   657   },
   659   create: function BStore_create(record) {
   660     // Default to unfiled if we don't have the parent yet.
   662     // Valid parent IDs are all positive integers. Other values -- undefined,
   663     // null, -1 -- all compare false for > 0, so this catches them all. We
   664     // don't just use <= without the !, because undefined and null compare
   665     // false for that, too!
   666     if (!(record._parent > 0)) {
   667       this._log.debug("Parent is " + record._parent + "; reparenting to unfiled.");
   668       record._parent = kSpecialIds.unfiled;
   669     }
   671     let newId;
   672     switch (record.type) {
   673     case "bookmark":
   674     case "query":
   675     case "microsummary": {
   676       let uri = Utils.makeURI(record.bmkUri);
   677       newId = PlacesUtils.bookmarks.insertBookmark(
   678         record._parent, uri, PlacesUtils.bookmarks.DEFAULT_INDEX, record.title);
   679       this._log.debug("created bookmark " + newId + " under " + record._parent
   680                       + " as " + record.title + " " + record.bmkUri);
   682       // Smart bookmark annotations are strings.
   683       if (record.queryId) {
   684         PlacesUtils.annotations.setItemAnnotation(
   685           newId, SMART_BOOKMARKS_ANNO, record.queryId, 0,
   686           PlacesUtils.annotations.EXPIRE_NEVER);
   687       }
   689       if (Array.isArray(record.tags)) {
   690         this._tagURI(uri, record.tags);
   691       }
   692       PlacesUtils.bookmarks.setKeywordForBookmark(newId, record.keyword);
   693       if (record.description) {
   694         PlacesUtils.annotations.setItemAnnotation(
   695           newId, DESCRIPTION_ANNO, record.description, 0,
   696           PlacesUtils.annotations.EXPIRE_NEVER);
   697       }
   699       if (record.loadInSidebar) {
   700         PlacesUtils.annotations.setItemAnnotation(
   701           newId, SIDEBAR_ANNO, true, 0,
   702           PlacesUtils.annotations.EXPIRE_NEVER);
   703       }
   705     } break;
   706     case "folder":
   707       newId = PlacesUtils.bookmarks.createFolder(
   708         record._parent, record.title, PlacesUtils.bookmarks.DEFAULT_INDEX);
   709       this._log.debug("created folder " + newId + " under " + record._parent
   710                       + " as " + record.title);
   712       if (record.description) {
   713         PlacesUtils.annotations.setItemAnnotation(
   714           newId, DESCRIPTION_ANNO, record.description, 0,
   715           PlacesUtils.annotations.EXPIRE_NEVER);
   716       }
   718       // record.children will be dealt with in _orderChildren.
   719       break;
   720     case "livemark":
   721       let siteURI = null;
   722       if (!record.feedUri) {
   723         this._log.debug("No feed URI: skipping livemark record " + record.id);
   724         return;
   725       }
   726       if (PlacesUtils.annotations
   727                      .itemHasAnnotation(record._parent, PlacesUtils.LMANNO_FEEDURI)) {
   728         this._log.debug("Invalid parent: skipping livemark record " + record.id);
   729         return;
   730       }
   732       if (record.siteUri != null)
   733         siteURI = Utils.makeURI(record.siteUri);
   735       // Until this engine can handle asynchronous error reporting, we need to
   736       // detect errors on creation synchronously.
   737       let spinningCb = Async.makeSpinningCallback();
   739       let livemarkObj = {title: record.title,
   740                          parentId: record._parent,
   741                          index: PlacesUtils.bookmarks.DEFAULT_INDEX,
   742                          feedURI: Utils.makeURI(record.feedUri),
   743                          siteURI: siteURI,
   744                          guid: record.id};
   745       PlacesUtils.livemarks.addLivemark(livemarkObj).then(
   746         aLivemark => { spinningCb(null, [Components.results.NS_OK, aLivemark]) },
   747         () => { spinningCb(null, [Components.results.NS_ERROR_UNEXPECTED, aLivemark]) }
   748       );
   750       let [status, livemark] = spinningCb.wait();
   751       if (!Components.isSuccessCode(status)) {
   752         throw status;
   753       }
   755       this._log.debug("Created livemark " + livemark.id + " under " +
   756                       livemark.parentId + " as " + livemark.title +
   757                       ", " + livemark.siteURI.spec + ", " +
   758                       livemark.feedURI.spec + ", GUID " +
   759                       livemark.guid);
   760       break;
   761     case "separator":
   762       newId = PlacesUtils.bookmarks.insertSeparator(
   763         record._parent, PlacesUtils.bookmarks.DEFAULT_INDEX);
   764       this._log.debug("created separator " + newId + " under " + record._parent);
   765       break;
   766     case "item":
   767       this._log.debug(" -> got a generic places item.. do nothing?");
   768       return;
   769     default:
   770       this._log.error("_create: Unknown item type: " + record.type);
   771       return;
   772     }
   774     if (newId) {
   775       // Livemarks can set the GUID through the API, so there's no need to
   776       // do that here.
   777       this._log.trace("Setting GUID of new item " + newId + " to " + record.id);
   778       this._setGUID(newId, record.id);
   779     }
   780   },
   782   // Factored out of `remove` to avoid redundant DB queries when the Places ID
   783   // is already known.
   784   removeById: function removeById(itemId, guid) {
   785     let type = PlacesUtils.bookmarks.getItemType(itemId);
   787     switch (type) {
   788     case PlacesUtils.bookmarks.TYPE_BOOKMARK:
   789       this._log.debug("  -> removing bookmark " + guid);
   790       PlacesUtils.bookmarks.removeItem(itemId);
   791       break;
   792     case PlacesUtils.bookmarks.TYPE_FOLDER:
   793       this._log.debug("  -> removing folder " + guid);
   794       PlacesUtils.bookmarks.removeItem(itemId);
   795       break;
   796     case PlacesUtils.bookmarks.TYPE_SEPARATOR:
   797       this._log.debug("  -> removing separator " + guid);
   798       PlacesUtils.bookmarks.removeItem(itemId);
   799       break;
   800     default:
   801       this._log.error("remove: Unknown item type: " + type);
   802       break;
   803     }
   804   },
   806   remove: function BStore_remove(record) {
   807     if (kSpecialIds.isSpecialGUID(record.id)) {
   808       this._log.warn("Refusing to remove special folder " + record.id);
   809       return;
   810     }
   812     let itemId = this.idForGUID(record.id);
   813     if (itemId <= 0) {
   814       this._log.debug("Item " + record.id + " already removed");
   815       return;
   816     }
   817     this.removeById(itemId, record.id);
   818   },
   820   _taggableTypes: ["bookmark", "microsummary", "query"],
   821   isTaggable: function isTaggable(recordType) {
   822     return this._taggableTypes.indexOf(recordType) != -1;
   823   },
   825   update: function BStore_update(record) {
   826     let itemId = this.idForGUID(record.id);
   828     if (itemId <= 0) {
   829       this._log.debug("Skipping update for unknown item: " + record.id);
   830       return;
   831     }
   833     // Two items are the same type if they have the same ItemType in Places,
   834     // and also share some key characteristics (e.g., both being livemarks).
   835     // We figure this out by examining the item to find the equivalent granular
   836     // (string) type.
   837     // If they're not the same type, we can't just update attributes. Delete
   838     // then recreate the record instead.
   839     let localItemType    = this._recordType(itemId);
   840     let remoteRecordType = record.type;
   841     this._log.trace("Local type: " + localItemType + ". " +
   842                     "Remote type: " + remoteRecordType + ".");
   844     if (localItemType != remoteRecordType) {
   845       this._log.debug("Local record and remote record differ in type. " +
   846                       "Deleting and recreating.");
   847       this.removeById(itemId, record.id);
   848       this.create(record);
   849       return;
   850     }
   852     this._log.trace("Updating " + record.id + " (" + itemId + ")");
   854     // Move the bookmark to a new parent or new position if necessary
   855     if (record._parent > 0 &&
   856         PlacesUtils.bookmarks.getFolderIdForItem(itemId) != record._parent) {
   857       this._reparentItem(itemId, record._parent);
   858     }
   860     for (let [key, val] in Iterator(record.cleartext)) {
   861       switch (key) {
   862       case "title":
   863         PlacesUtils.bookmarks.setItemTitle(itemId, val);
   864         break;
   865       case "bmkUri":
   866         PlacesUtils.bookmarks.changeBookmarkURI(itemId, Utils.makeURI(val));
   867         break;
   868       case "tags":
   869         if (Array.isArray(val)) {
   870           if (this.isTaggable(remoteRecordType)) {
   871             this._tagID(itemId, val);
   872           } else {
   873             this._log.debug("Remote record type is invalid for tags: " + remoteRecordType);
   874           }
   875         }
   876         break;
   877       case "keyword":
   878         PlacesUtils.bookmarks.setKeywordForBookmark(itemId, val);
   879         break;
   880       case "description":
   881         if (val) {
   882           PlacesUtils.annotations.setItemAnnotation(
   883             itemId, DESCRIPTION_ANNO, val, 0,
   884             PlacesUtils.annotations.EXPIRE_NEVER);
   885         } else {
   886           PlacesUtils.annotations.removeItemAnnotation(itemId, DESCRIPTION_ANNO);
   887         }
   888         break;
   889       case "loadInSidebar":
   890         if (val) {
   891           PlacesUtils.annotations.setItemAnnotation(
   892             itemId, SIDEBAR_ANNO, true, 0,
   893             PlacesUtils.annotations.EXPIRE_NEVER);
   894         } else {
   895           PlacesUtils.annotations.removeItemAnnotation(itemId, SIDEBAR_ANNO);
   896         }
   897         break;
   898       case "queryId":
   899         PlacesUtils.annotations.setItemAnnotation(
   900           itemId, SMART_BOOKMARKS_ANNO, val, 0,
   901           PlacesUtils.annotations.EXPIRE_NEVER);
   902         break;
   903       }
   904     }
   905   },
   907   _orderChildren: function _orderChildren() {
   908     for (let [guid, children] in Iterator(this._childrenToOrder)) {
   909       // Reorder children according to the GUID list. Gracefully deal
   910       // with missing items, e.g. locally deleted.
   911       let delta = 0;
   912       let parent = null;
   913       for (let idx = 0; idx < children.length; idx++) {
   914         let itemid = this.idForGUID(children[idx]);
   915         if (itemid == -1) {
   916           delta += 1;
   917           this._log.trace("Could not locate record " + children[idx]);
   918           continue;
   919         }
   920         try {
   921           // This code path could be optimized by caching the parent earlier.
   922           // Doing so should take in count any edge case due to reparenting
   923           // or parent invalidations though.
   924           if (!parent) {
   925             parent = PlacesUtils.bookmarks.getFolderIdForItem(itemid);
   926           }
   927           PlacesUtils.bookmarks.moveItem(itemid, parent, idx - delta);
   928         } catch (ex) {
   929           this._log.debug("Could not move item " + children[idx] + ": " + ex);
   930         }
   931       }
   932     }
   933   },
   935   changeItemID: function BStore_changeItemID(oldID, newID) {
   936     this._log.debug("Changing GUID " + oldID + " to " + newID);
   938     // Make sure there's an item to change GUIDs
   939     let itemId = this.idForGUID(oldID);
   940     if (itemId <= 0)
   941       return;
   943     this._setGUID(itemId, newID);
   944   },
   946   _getNode: function BStore__getNode(folder) {
   947     let query = PlacesUtils.history.getNewQuery();
   948     query.setFolders([folder], 1);
   949     return PlacesUtils.history.executeQuery(
   950       query, PlacesUtils.history.getNewQueryOptions()).root;
   951   },
   953   _getTags: function BStore__getTags(uri) {
   954     try {
   955       if (typeof(uri) == "string")
   956         uri = Utils.makeURI(uri);
   957     } catch(e) {
   958       this._log.warn("Could not parse URI \"" + uri + "\": " + e);
   959     }
   960     return PlacesUtils.tagging.getTagsForURI(uri, {});
   961   },
   963   _getDescription: function BStore__getDescription(id) {
   964     try {
   965       return PlacesUtils.annotations.getItemAnnotation(id, DESCRIPTION_ANNO);
   966     } catch (e) {
   967       return null;
   968     }
   969   },
   971   _isLoadInSidebar: function BStore__isLoadInSidebar(id) {
   972     return PlacesUtils.annotations.itemHasAnnotation(id, SIDEBAR_ANNO);
   973   },
   975   get _childGUIDsStm() {
   976     return this._getStmt(
   977       "SELECT id AS item_id, guid " +
   978       "FROM moz_bookmarks " +
   979       "WHERE parent = :parent " +
   980       "ORDER BY position");
   981   },
   982   _childGUIDsCols: ["item_id", "guid"],
   984   _getChildGUIDsForId: function _getChildGUIDsForId(itemid) {
   985     let stmt = this._childGUIDsStm;
   986     stmt.params.parent = itemid;
   987     let rows = Async.querySpinningly(stmt, this._childGUIDsCols);
   988     return rows.map(function (row) {
   989       if (row.guid) {
   990         return row.guid;
   991       }
   992       // A GUID hasn't been assigned to this item yet, do this now.
   993       return this.GUIDForId(row.item_id);
   994     }, this);
   995   },
   997   // Create a record starting from the weave id (places guid)
   998   createRecord: function createRecord(id, collection) {
   999     let placeId = this.idForGUID(id);
  1000     let record;
  1001     if (placeId <= 0) { // deleted item
  1002       record = new PlacesItem(collection, id);
  1003       record.deleted = true;
  1004       return record;
  1007     let parent = PlacesUtils.bookmarks.getFolderIdForItem(placeId);
  1008     switch (PlacesUtils.bookmarks.getItemType(placeId)) {
  1009     case PlacesUtils.bookmarks.TYPE_BOOKMARK:
  1010       let bmkUri = PlacesUtils.bookmarks.getBookmarkURI(placeId).spec;
  1011       if (bmkUri.indexOf("place:") == 0) {
  1012         record = new BookmarkQuery(collection, id);
  1014         // Get the actual tag name instead of the local itemId
  1015         let folder = bmkUri.match(/[:&]folder=(\d+)/);
  1016         try {
  1017           // There might not be the tag yet when creating on a new client
  1018           if (folder != null) {
  1019             folder = folder[1];
  1020             record.folderName = PlacesUtils.bookmarks.getItemTitle(folder);
  1021             this._log.trace("query id: " + folder + " = " + record.folderName);
  1024         catch(ex) {}
  1026         // Persist the Smart Bookmark anno, if found.
  1027         try {
  1028           let anno = PlacesUtils.annotations.getItemAnnotation(placeId, SMART_BOOKMARKS_ANNO);
  1029           if (anno != null) {
  1030             this._log.trace("query anno: " + SMART_BOOKMARKS_ANNO +
  1031                             " = " + anno);
  1032             record.queryId = anno;
  1035         catch(ex) {}
  1037       else {
  1038         record = new Bookmark(collection, id);
  1040       record.title = PlacesUtils.bookmarks.getItemTitle(placeId);
  1042       record.parentName = PlacesUtils.bookmarks.getItemTitle(parent);
  1043       record.bmkUri = bmkUri;
  1044       record.tags = this._getTags(record.bmkUri);
  1045       record.keyword = PlacesUtils.bookmarks.getKeywordForBookmark(placeId);
  1046       record.description = this._getDescription(placeId);
  1047       record.loadInSidebar = this._isLoadInSidebar(placeId);
  1048       break;
  1050     case PlacesUtils.bookmarks.TYPE_FOLDER:
  1051       if (PlacesUtils.annotations
  1052                      .itemHasAnnotation(placeId, PlacesUtils.LMANNO_FEEDURI)) {
  1053         record = new Livemark(collection, id);
  1054         let as = PlacesUtils.annotations;
  1055         record.feedUri = as.getItemAnnotation(placeId, PlacesUtils.LMANNO_FEEDURI);
  1056         try {
  1057           record.siteUri = as.getItemAnnotation(placeId, PlacesUtils.LMANNO_SITEURI);
  1058         } catch (ex) {}
  1059       } else {
  1060         record = new BookmarkFolder(collection, id);
  1063       if (parent > 0)
  1064         record.parentName = PlacesUtils.bookmarks.getItemTitle(parent);
  1065       record.title = PlacesUtils.bookmarks.getItemTitle(placeId);
  1066       record.description = this._getDescription(placeId);
  1067       record.children = this._getChildGUIDsForId(placeId);
  1068       break;
  1070     case PlacesUtils.bookmarks.TYPE_SEPARATOR:
  1071       record = new BookmarkSeparator(collection, id);
  1072       if (parent > 0)
  1073         record.parentName = PlacesUtils.bookmarks.getItemTitle(parent);
  1074       // Create a positioning identifier for the separator, used by _mapDupe
  1075       record.pos = PlacesUtils.bookmarks.getItemIndex(placeId);
  1076       break;
  1078     default:
  1079       record = new PlacesItem(collection, id);
  1080       this._log.warn("Unknown item type, cannot serialize: " +
  1081                      PlacesUtils.bookmarks.getItemType(placeId));
  1084     record.parentid = this.GUIDForId(parent);
  1085     record.sortindex = this._calculateIndex(record);
  1087     return record;
  1088   },
  1090   _stmts: {},
  1091   _getStmt: function(query) {
  1092     if (query in this._stmts) {
  1093       return this._stmts[query];
  1096     this._log.trace("Creating SQL statement: " + query);
  1097     let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
  1098                         .DBConnection;
  1099     return this._stmts[query] = db.createAsyncStatement(query);
  1100   },
  1102   get _frecencyStm() {
  1103     return this._getStmt(
  1104         "SELECT frecency " +
  1105         "FROM moz_places " +
  1106         "WHERE url = :url " +
  1107         "LIMIT 1");
  1108   },
  1109   _frecencyCols: ["frecency"],
  1111   get _setGUIDStm() {
  1112     return this._getStmt(
  1113       "UPDATE moz_bookmarks " +
  1114       "SET guid = :guid " +
  1115       "WHERE id = :item_id");
  1116   },
  1118   // Some helper functions to handle GUIDs
  1119   _setGUID: function _setGUID(id, guid) {
  1120     if (!guid)
  1121       guid = Utils.makeGUID();
  1123     let stmt = this._setGUIDStm;
  1124     stmt.params.guid = guid;
  1125     stmt.params.item_id = id;
  1126     Async.querySpinningly(stmt);
  1127     return guid;
  1128   },
  1130   get _guidForIdStm() {
  1131     return this._getStmt(
  1132       "SELECT guid " +
  1133       "FROM moz_bookmarks " +
  1134       "WHERE id = :item_id");
  1135   },
  1136   _guidForIdCols: ["guid"],
  1138   GUIDForId: function GUIDForId(id) {
  1139     let special = kSpecialIds.specialGUIDForId(id);
  1140     if (special)
  1141       return special;
  1143     let stmt = this._guidForIdStm;
  1144     stmt.params.item_id = id;
  1146     // Use the existing GUID if it exists
  1147     let result = Async.querySpinningly(stmt, this._guidForIdCols)[0];
  1148     if (result && result.guid)
  1149       return result.guid;
  1151     // Give the uri a GUID if it doesn't have one
  1152     return this._setGUID(id);
  1153   },
  1155   get _idForGUIDStm() {
  1156     return this._getStmt(
  1157       "SELECT id AS item_id " +
  1158       "FROM moz_bookmarks " +
  1159       "WHERE guid = :guid");
  1160   },
  1161   _idForGUIDCols: ["item_id"],
  1163   // noCreate is provided as an optional argument to prevent the creation of
  1164   // non-existent special records, such as "mobile".
  1165   idForGUID: function idForGUID(guid, noCreate) {
  1166     if (kSpecialIds.isSpecialGUID(guid))
  1167       return kSpecialIds.specialIdForGUID(guid, !noCreate);
  1169     let stmt = this._idForGUIDStm;
  1170     // guid might be a String object rather than a string.
  1171     stmt.params.guid = guid.toString();
  1173     let results = Async.querySpinningly(stmt, this._idForGUIDCols);
  1174     this._log.trace("Number of rows matching GUID " + guid + ": "
  1175                     + results.length);
  1177     // Here's the one we care about: the first.
  1178     let result = results[0];
  1180     if (!result)
  1181       return -1;
  1183     return result.item_id;
  1184   },
  1186   _calculateIndex: function _calculateIndex(record) {
  1187     // Ensure folders have a very high sort index so they're not synced last.
  1188     if (record.type == "folder")
  1189       return FOLDER_SORTINDEX;
  1191     // For anything directly under the toolbar, give it a boost of more than an
  1192     // unvisited bookmark
  1193     let index = 0;
  1194     if (record.parentid == "toolbar")
  1195       index += 150;
  1197     // Add in the bookmark's frecency if we have something.
  1198     if (record.bmkUri != null) {
  1199       this._frecencyStm.params.url = record.bmkUri;
  1200       let result = Async.querySpinningly(this._frecencyStm, this._frecencyCols);
  1201       if (result.length)
  1202         index += result[0].frecency;
  1205     return index;
  1206   },
  1208   _getChildren: function BStore_getChildren(guid, items) {
  1209     let node = guid; // the recursion case
  1210     if (typeof(node) == "string") { // callers will give us the guid as the first arg
  1211       let nodeID = this.idForGUID(guid, true);
  1212       if (!nodeID) {
  1213         this._log.debug("No node for GUID " + guid + "; returning no children.");
  1214         return items;
  1216       node = this._getNode(nodeID);
  1219     if (node.type == node.RESULT_TYPE_FOLDER) {
  1220       node.QueryInterface(Ci.nsINavHistoryQueryResultNode);
  1221       node.containerOpen = true;
  1222       try {
  1223         // Remember all the children GUIDs and recursively get more
  1224         for (let i = 0; i < node.childCount; i++) {
  1225           let child = node.getChild(i);
  1226           items[this.GUIDForId(child.itemId)] = true;
  1227           this._getChildren(child, items);
  1230       finally {
  1231         node.containerOpen = false;
  1235     return items;
  1236   },
  1238   /**
  1239    * Associates the URI of the item with the provided ID with the
  1240    * provided array of tags.
  1241    * If the provided ID does not identify an item with a URI,
  1242    * returns immediately.
  1243    */
  1244   _tagID: function _tagID(itemID, tags) {
  1245     if (!itemID || !tags) {
  1246       return;
  1249     try {
  1250       let u = PlacesUtils.bookmarks.getBookmarkURI(itemID);
  1251       this._tagURI(u, tags);
  1252     } catch (e) {
  1253       this._log.warn("Got exception fetching URI for " + itemID + ": not tagging. " +
  1254                      Utils.exceptionStr(e));
  1256       // I guess it doesn't have a URI. Don't try to tag it.
  1257       return;
  1259   },
  1261   /**
  1262    * Associate the provided URI with the provided array of tags.
  1263    * If the provided URI is falsy, returns immediately.
  1264    */
  1265   _tagURI: function _tagURI(bookmarkURI, tags) {
  1266     if (!bookmarkURI || !tags) {
  1267       return;
  1270     // Filter out any null/undefined/empty tags.
  1271     tags = tags.filter(function(t) t);
  1273     // Temporarily tag a dummy URI to preserve tag ids when untagging.
  1274     let dummyURI = Utils.makeURI("about:weave#BStore_tagURI");
  1275     PlacesUtils.tagging.tagURI(dummyURI, tags);
  1276     PlacesUtils.tagging.untagURI(bookmarkURI, null);
  1277     PlacesUtils.tagging.tagURI(bookmarkURI, tags);
  1278     PlacesUtils.tagging.untagURI(dummyURI, null);
  1279   },
  1281   getAllIDs: function BStore_getAllIDs() {
  1282     let items = {"menu": true,
  1283                  "toolbar": true};
  1284     for each (let guid in kSpecialIds.guids) {
  1285       if (guid != "places" && guid != "tags")
  1286         this._getChildren(guid, items);
  1288     return items;
  1289   },
  1291   wipe: function BStore_wipe() {
  1292     let cb = Async.makeSpinningCallback();
  1293     Task.spawn(function() {
  1294       // Save a backup before clearing out all bookmarks.
  1295       yield PlacesBackups.create(null, true);
  1296       for each (let guid in kSpecialIds.guids)
  1297         if (guid != "places") {
  1298           let id = kSpecialIds.specialIdForGUID(guid);
  1299           if (id)
  1300             PlacesUtils.bookmarks.removeFolderChildren(id);
  1302       cb();
  1303     });
  1304     cb.wait();
  1306 };
  1308 function BookmarksTracker(name, engine) {
  1309   Tracker.call(this, name, engine);
  1311   Svc.Obs.add("places-shutdown", this);
  1313 BookmarksTracker.prototype = {
  1314   __proto__: Tracker.prototype,
  1316   startTracking: function() {
  1317     PlacesUtils.bookmarks.addObserver(this, true);
  1318     Svc.Obs.add("bookmarks-restore-begin", this);
  1319     Svc.Obs.add("bookmarks-restore-success", this);
  1320     Svc.Obs.add("bookmarks-restore-failed", this);
  1321   },
  1323   stopTracking: function() {
  1324     PlacesUtils.bookmarks.removeObserver(this);
  1325     Svc.Obs.remove("bookmarks-restore-begin", this);
  1326     Svc.Obs.remove("bookmarks-restore-success", this);
  1327     Svc.Obs.remove("bookmarks-restore-failed", this);
  1328   },
  1330   observe: function observe(subject, topic, data) {
  1331     Tracker.prototype.observe.call(this, subject, topic, data);
  1333     switch (topic) {
  1334       case "bookmarks-restore-begin":
  1335         this._log.debug("Ignoring changes from importing bookmarks.");
  1336         this.ignoreAll = true;
  1337         break;
  1338       case "bookmarks-restore-success":
  1339         this._log.debug("Tracking all items on successful import.");
  1340         this.ignoreAll = false;
  1342         this._log.debug("Restore succeeded: wiping server and other clients.");
  1343         this.engine.service.resetClient([this.name]);
  1344         this.engine.service.wipeServer([this.name]);
  1345         this.engine.service.clientsEngine.sendCommand("wipeEngine", [this.name]);
  1346         break;
  1347       case "bookmarks-restore-failed":
  1348         this._log.debug("Tracking all items on failed import.");
  1349         this.ignoreAll = false;
  1350         break;
  1352   },
  1354   QueryInterface: XPCOMUtils.generateQI([
  1355     Ci.nsINavBookmarkObserver,
  1356     Ci.nsINavBookmarkObserver_MOZILLA_1_9_1_ADDITIONS,
  1357     Ci.nsISupportsWeakReference
  1358   ]),
  1360   /**
  1361    * Add a bookmark GUID to be uploaded and bump up the sync score.
  1363    * @param itemGuid
  1364    *        GUID of the bookmark to upload.
  1365    */
  1366   _add: function BMT__add(itemId, guid) {
  1367     guid = kSpecialIds.specialGUIDForId(itemId) || guid;
  1368     if (this.addChangedID(guid))
  1369       this._upScore();
  1370   },
  1372   /* Every add/remove/change will trigger a sync for MULTI_DEVICE. */
  1373   _upScore: function BMT__upScore() {
  1374     this.score += SCORE_INCREMENT_XLARGE;
  1375   },
  1377   /**
  1378    * Determine if a change should be ignored.
  1380    * @param itemId
  1381    *        Item under consideration to ignore
  1382    * @param folder (optional)
  1383    *        Folder of the item being changed
  1384    */
  1385   _ignore: function BMT__ignore(itemId, folder, guid) {
  1386     // Ignore unconditionally if the engine tells us to.
  1387     if (this.ignoreAll)
  1388       return true;
  1390     // Get the folder id if we weren't given one.
  1391     if (folder == null) {
  1392       try {
  1393         folder = PlacesUtils.bookmarks.getFolderIdForItem(itemId);
  1394       } catch (ex) {
  1395         this._log.debug("getFolderIdForItem(" + itemId +
  1396                         ") threw; calling _ensureMobileQuery.");
  1397         // I'm guessing that gFIFI can throw, and perhaps that's why
  1398         // _ensureMobileQuery is here at all. Try not to call it.
  1399         this._ensureMobileQuery();
  1400         folder = PlacesUtils.bookmarks.getFolderIdForItem(itemId);
  1404     // Ignore changes to tags (folders under the tags folder).
  1405     let tags = kSpecialIds.tags;
  1406     if (folder == tags)
  1407       return true;
  1409     // Ignore tag items (the actual instance of a tag for a bookmark).
  1410     if (PlacesUtils.bookmarks.getFolderIdForItem(folder) == tags)
  1411       return true;
  1413     // Make sure to remove items that have the exclude annotation.
  1414     if (PlacesUtils.annotations.itemHasAnnotation(itemId, EXCLUDEBACKUP_ANNO)) {
  1415       this.removeChangedID(guid);
  1416       return true;
  1419     return false;
  1420   },
  1422   onItemAdded: function BMT_onItemAdded(itemId, folder, index,
  1423                                         itemType, uri, title, dateAdded,
  1424                                         guid, parentGuid) {
  1425     if (this._ignore(itemId, folder, guid))
  1426       return;
  1428     this._log.trace("onItemAdded: " + itemId);
  1429     this._add(itemId, guid);
  1430     this._add(folder, parentGuid);
  1431   },
  1433   onItemRemoved: function (itemId, parentId, index, type, uri,
  1434                            guid, parentGuid) {
  1435     if (this._ignore(itemId, parentId, guid)) {
  1436       return;
  1439     this._log.trace("onItemRemoved: " + itemId);
  1440     this._add(itemId, guid);
  1441     this._add(parentId, parentGuid);
  1442   },
  1444   _ensureMobileQuery: function _ensureMobileQuery() {
  1445     let find = function (val)
  1446       PlacesUtils.annotations.getItemsWithAnnotation(ORGANIZERQUERY_ANNO, {}).filter(
  1447         function (id) PlacesUtils.annotations.getItemAnnotation(id, ORGANIZERQUERY_ANNO) == val
  1448       );
  1450     // Don't continue if the Library isn't ready
  1451     let all = find(ALLBOOKMARKS_ANNO);
  1452     if (all.length == 0)
  1453       return;
  1455     // Disable handling of notifications while changing the mobile query
  1456     this.ignoreAll = true;
  1458     let mobile = find(MOBILE_ANNO);
  1459     let queryURI = Utils.makeURI("place:folder=" + kSpecialIds.mobile);
  1460     let title = Str.sync.get("mobile.label");
  1462     // Don't add OR remove the mobile bookmarks if there's nothing.
  1463     if (PlacesUtils.bookmarks.getIdForItemAt(kSpecialIds.mobile, 0) == -1) {
  1464       if (mobile.length != 0)
  1465         PlacesUtils.bookmarks.removeItem(mobile[0]);
  1467     // Add the mobile bookmarks query if it doesn't exist
  1468     else if (mobile.length == 0) {
  1469       let query = PlacesUtils.bookmarks.insertBookmark(all[0], queryURI, -1, title);
  1470       PlacesUtils.annotations.setItemAnnotation(query, ORGANIZERQUERY_ANNO, MOBILE_ANNO, 0,
  1471                                   PlacesUtils.annotations.EXPIRE_NEVER);
  1472       PlacesUtils.annotations.setItemAnnotation(query, EXCLUDEBACKUP_ANNO, 1, 0,
  1473                                   PlacesUtils.annotations.EXPIRE_NEVER);
  1475     // Make sure the existing title is correct
  1476     else if (PlacesUtils.bookmarks.getItemTitle(mobile[0]) != title) {
  1477       PlacesUtils.bookmarks.setItemTitle(mobile[0], title);
  1480     this.ignoreAll = false;
  1481   },
  1483   // This method is oddly structured, but the idea is to return as quickly as
  1484   // possible -- this handler gets called *every time* a bookmark changes, for
  1485   // *each change*.
  1486   onItemChanged: function BMT_onItemChanged(itemId, property, isAnno, value,
  1487                                             lastModified, itemType, parentId,
  1488                                             guid, parentGuid) {
  1489     // Quicker checks first.
  1490     if (this.ignoreAll)
  1491       return;
  1493     if (isAnno && (ANNOS_TO_TRACK.indexOf(property) == -1))
  1494       // Ignore annotations except for the ones that we sync.
  1495       return;
  1497     // Ignore favicon changes to avoid unnecessary churn.
  1498     if (property == "favicon")
  1499       return;
  1501     if (this._ignore(itemId, parentId, guid))
  1502       return;
  1504     this._log.trace("onItemChanged: " + itemId +
  1505                     (", " + property + (isAnno? " (anno)" : "")) +
  1506                     (value ? (" = \"" + value + "\"") : ""));
  1507     this._add(itemId, guid);
  1508   },
  1510   onItemMoved: function BMT_onItemMoved(itemId, oldParent, oldIndex,
  1511                                         newParent, newIndex, itemType,
  1512                                         guid, oldParentGuid, newParentGuid) {
  1513     if (this._ignore(itemId, newParent, guid))
  1514       return;
  1516     this._log.trace("onItemMoved: " + itemId);
  1517     this._add(oldParent, oldParentGuid);
  1518     if (oldParent != newParent) {
  1519       this._add(itemId, guid);
  1520       this._add(newParent, newParentGuid);
  1523     // Remove any position annotations now that the user moved the item
  1524     PlacesUtils.annotations.removeItemAnnotation(itemId, PARENT_ANNO);
  1525   },
  1527   onBeginUpdateBatch: function () {},
  1528   onEndUpdateBatch: function () {},
  1529   onItemVisited: function () {}
  1530 };

mercurial