services/sync/modules/engines/bookmarks.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/services/sync/modules/engines/bookmarks.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1530 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +this.EXPORTED_SYMBOLS = ['BookmarksEngine', "PlacesItem", "Bookmark",
     1.9 +                         "BookmarkFolder", "BookmarkQuery",
    1.10 +                         "Livemark", "BookmarkSeparator"];
    1.11 +
    1.12 +const Cc = Components.classes;
    1.13 +const Ci = Components.interfaces;
    1.14 +const Cu = Components.utils;
    1.15 +
    1.16 +Cu.import("resource://gre/modules/PlacesUtils.jsm");
    1.17 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.18 +Cu.import("resource://services-common/async.js");
    1.19 +Cu.import("resource://services-sync/constants.js");
    1.20 +Cu.import("resource://services-sync/engines.js");
    1.21 +Cu.import("resource://services-sync/record.js");
    1.22 +Cu.import("resource://services-sync/util.js");
    1.23 +Cu.import("resource://gre/modules/Task.jsm");
    1.24 +Cu.import("resource://gre/modules/PlacesBackups.jsm");
    1.25 +
    1.26 +const ALLBOOKMARKS_ANNO    = "AllBookmarks";
    1.27 +const DESCRIPTION_ANNO     = "bookmarkProperties/description";
    1.28 +const SIDEBAR_ANNO         = "bookmarkProperties/loadInSidebar";
    1.29 +const MOBILEROOT_ANNO      = "mobile/bookmarksRoot";
    1.30 +const MOBILE_ANNO          = "MobileBookmarks";
    1.31 +const EXCLUDEBACKUP_ANNO   = "places/excludeFromBackup";
    1.32 +const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
    1.33 +const PARENT_ANNO          = "sync/parent";
    1.34 +const ORGANIZERQUERY_ANNO  = "PlacesOrganizer/OrganizerQuery";
    1.35 +const ANNOS_TO_TRACK = [DESCRIPTION_ANNO, SIDEBAR_ANNO,
    1.36 +                        PlacesUtils.LMANNO_FEEDURI, PlacesUtils.LMANNO_SITEURI];
    1.37 +
    1.38 +const SERVICE_NOT_SUPPORTED = "Service not supported on this platform";
    1.39 +const FOLDER_SORTINDEX = 1000000;
    1.40 +
    1.41 +this.PlacesItem = function PlacesItem(collection, id, type) {
    1.42 +  CryptoWrapper.call(this, collection, id);
    1.43 +  this.type = type || "item";
    1.44 +}
    1.45 +PlacesItem.prototype = {
    1.46 +  decrypt: function PlacesItem_decrypt(keyBundle) {
    1.47 +    // Do the normal CryptoWrapper decrypt, but change types before returning
    1.48 +    let clear = CryptoWrapper.prototype.decrypt.call(this, keyBundle);
    1.49 +
    1.50 +    // Convert the abstract places item to the actual object type
    1.51 +    if (!this.deleted)
    1.52 +      this.__proto__ = this.getTypeObject(this.type).prototype;
    1.53 +
    1.54 +    return clear;
    1.55 +  },
    1.56 +
    1.57 +  getTypeObject: function PlacesItem_getTypeObject(type) {
    1.58 +    switch (type) {
    1.59 +      case "bookmark":
    1.60 +      case "microsummary":
    1.61 +        return Bookmark;
    1.62 +      case "query":
    1.63 +        return BookmarkQuery;
    1.64 +      case "folder":
    1.65 +        return BookmarkFolder;
    1.66 +      case "livemark":
    1.67 +        return Livemark;
    1.68 +      case "separator":
    1.69 +        return BookmarkSeparator;
    1.70 +      case "item":
    1.71 +        return PlacesItem;
    1.72 +    }
    1.73 +    throw "Unknown places item object type: " + type;
    1.74 +  },
    1.75 +
    1.76 +  __proto__: CryptoWrapper.prototype,
    1.77 +  _logName: "Sync.Record.PlacesItem",
    1.78 +};
    1.79 +
    1.80 +Utils.deferGetSet(PlacesItem,
    1.81 +                  "cleartext",
    1.82 +                  ["hasDupe", "parentid", "parentName", "type"]);
    1.83 +
    1.84 +this.Bookmark = function Bookmark(collection, id, type) {
    1.85 +  PlacesItem.call(this, collection, id, type || "bookmark");
    1.86 +}
    1.87 +Bookmark.prototype = {
    1.88 +  __proto__: PlacesItem.prototype,
    1.89 +  _logName: "Sync.Record.Bookmark",
    1.90 +};
    1.91 +
    1.92 +Utils.deferGetSet(Bookmark,
    1.93 +                  "cleartext",
    1.94 +                  ["title", "bmkUri", "description",
    1.95 +                   "loadInSidebar", "tags", "keyword"]);
    1.96 +
    1.97 +this.BookmarkQuery = function BookmarkQuery(collection, id) {
    1.98 +  Bookmark.call(this, collection, id, "query");
    1.99 +}
   1.100 +BookmarkQuery.prototype = {
   1.101 +  __proto__: Bookmark.prototype,
   1.102 +  _logName: "Sync.Record.BookmarkQuery",
   1.103 +};
   1.104 +
   1.105 +Utils.deferGetSet(BookmarkQuery,
   1.106 +                  "cleartext",
   1.107 +                  ["folderName", "queryId"]);
   1.108 +
   1.109 +this.BookmarkFolder = function BookmarkFolder(collection, id, type) {
   1.110 +  PlacesItem.call(this, collection, id, type || "folder");
   1.111 +}
   1.112 +BookmarkFolder.prototype = {
   1.113 +  __proto__: PlacesItem.prototype,
   1.114 +  _logName: "Sync.Record.Folder",
   1.115 +};
   1.116 +
   1.117 +Utils.deferGetSet(BookmarkFolder, "cleartext", ["description", "title",
   1.118 +                                                "children"]);
   1.119 +
   1.120 +this.Livemark = function Livemark(collection, id) {
   1.121 +  BookmarkFolder.call(this, collection, id, "livemark");
   1.122 +}
   1.123 +Livemark.prototype = {
   1.124 +  __proto__: BookmarkFolder.prototype,
   1.125 +  _logName: "Sync.Record.Livemark",
   1.126 +};
   1.127 +
   1.128 +Utils.deferGetSet(Livemark, "cleartext", ["siteUri", "feedUri"]);
   1.129 +
   1.130 +this.BookmarkSeparator = function BookmarkSeparator(collection, id) {
   1.131 +  PlacesItem.call(this, collection, id, "separator");
   1.132 +}
   1.133 +BookmarkSeparator.prototype = {
   1.134 +  __proto__: PlacesItem.prototype,
   1.135 +  _logName: "Sync.Record.Separator",
   1.136 +};
   1.137 +
   1.138 +Utils.deferGetSet(BookmarkSeparator, "cleartext", "pos");
   1.139 +
   1.140 +
   1.141 +let kSpecialIds = {
   1.142 +
   1.143 +  // Special IDs. Note that mobile can attempt to create a record on
   1.144 +  // dereference; special accessors are provided to prevent recursion within
   1.145 +  // observers.
   1.146 +  guids: ["menu", "places", "tags", "toolbar", "unfiled", "mobile"],
   1.147 +
   1.148 +  // Create the special mobile folder to store mobile bookmarks.
   1.149 +  createMobileRoot: function createMobileRoot() {
   1.150 +    let root = PlacesUtils.placesRootId;
   1.151 +    let mRoot = PlacesUtils.bookmarks.createFolder(root, "mobile", -1);
   1.152 +    PlacesUtils.annotations.setItemAnnotation(
   1.153 +      mRoot, MOBILEROOT_ANNO, 1, 0, PlacesUtils.annotations.EXPIRE_NEVER);
   1.154 +    PlacesUtils.annotations.setItemAnnotation(
   1.155 +      mRoot, EXCLUDEBACKUP_ANNO, 1, 0, PlacesUtils.annotations.EXPIRE_NEVER);
   1.156 +    return mRoot;
   1.157 +  },
   1.158 +
   1.159 +  findMobileRoot: function findMobileRoot(create) {
   1.160 +    // Use the (one) mobile root if it already exists.
   1.161 +    let root = PlacesUtils.annotations.getItemsWithAnnotation(MOBILEROOT_ANNO, {});
   1.162 +    if (root.length != 0)
   1.163 +      return root[0];
   1.164 +
   1.165 +    if (create)
   1.166 +      return this.createMobileRoot();
   1.167 +
   1.168 +    return null;
   1.169 +  },
   1.170 +
   1.171 +  // Accessors for IDs.
   1.172 +  isSpecialGUID: function isSpecialGUID(g) {
   1.173 +    return this.guids.indexOf(g) != -1;
   1.174 +  },
   1.175 +
   1.176 +  specialIdForGUID: function specialIdForGUID(guid, create) {
   1.177 +    if (guid == "mobile") {
   1.178 +      return this.findMobileRoot(create);
   1.179 +    }
   1.180 +    return this[guid];
   1.181 +  },
   1.182 +
   1.183 +  // Don't bother creating mobile: if it doesn't exist, this ID can't be it!
   1.184 +  specialGUIDForId: function specialGUIDForId(id) {
   1.185 +    for each (let guid in this.guids)
   1.186 +      if (this.specialIdForGUID(guid, false) == id)
   1.187 +        return guid;
   1.188 +    return null;
   1.189 +  },
   1.190 +
   1.191 +  get menu()    PlacesUtils.bookmarksMenuFolderId,
   1.192 +  get places()  PlacesUtils.placesRootId,
   1.193 +  get tags()    PlacesUtils.tagsFolderId,
   1.194 +  get toolbar() PlacesUtils.toolbarFolderId,
   1.195 +  get unfiled() PlacesUtils.unfiledBookmarksFolderId,
   1.196 +  get mobile()  this.findMobileRoot(true),
   1.197 +};
   1.198 +
   1.199 +this.BookmarksEngine = function BookmarksEngine(service) {
   1.200 +  SyncEngine.call(this, "Bookmarks", service);
   1.201 +}
   1.202 +BookmarksEngine.prototype = {
   1.203 +  __proto__: SyncEngine.prototype,
   1.204 +  _recordObj: PlacesItem,
   1.205 +  _storeObj: BookmarksStore,
   1.206 +  _trackerObj: BookmarksTracker,
   1.207 +  version: 2,
   1.208 +
   1.209 +  _sync: function _sync() {
   1.210 +    let engine = this;
   1.211 +    let batchEx = null;
   1.212 +
   1.213 +    // Try running sync in batch mode
   1.214 +    PlacesUtils.bookmarks.runInBatchMode({
   1.215 +      runBatched: function wrappedSync() {
   1.216 +        try {
   1.217 +          SyncEngine.prototype._sync.call(engine);
   1.218 +        }
   1.219 +        catch(ex) {
   1.220 +          batchEx = ex;
   1.221 +        }
   1.222 +      }
   1.223 +    }, null);
   1.224 +
   1.225 +    // Expose the exception if something inside the batch failed
   1.226 +    if (batchEx != null) {
   1.227 +      throw batchEx;
   1.228 +    }
   1.229 +  },
   1.230 +
   1.231 +  _guidMapFailed: false,
   1.232 +  _buildGUIDMap: function _buildGUIDMap() {
   1.233 +    let guidMap = {};
   1.234 +    for (let guid in this._store.getAllIDs()) {
   1.235 +      // Figure out with which key to store the mapping.
   1.236 +      let key;
   1.237 +      let id = this._store.idForGUID(guid);
   1.238 +      switch (PlacesUtils.bookmarks.getItemType(id)) {
   1.239 +        case PlacesUtils.bookmarks.TYPE_BOOKMARK:
   1.240 +
   1.241 +          // Smart bookmarks map to their annotation value.
   1.242 +          let queryId;
   1.243 +          try {
   1.244 +            queryId = PlacesUtils.annotations.getItemAnnotation(
   1.245 +              id, SMART_BOOKMARKS_ANNO);
   1.246 +          } catch(ex) {}
   1.247 +          
   1.248 +          if (queryId)
   1.249 +            key = "q" + queryId;
   1.250 +          else
   1.251 +            key = "b" + PlacesUtils.bookmarks.getBookmarkURI(id).spec + ":" +
   1.252 +                  PlacesUtils.bookmarks.getItemTitle(id);
   1.253 +          break;
   1.254 +        case PlacesUtils.bookmarks.TYPE_FOLDER:
   1.255 +          key = "f" + PlacesUtils.bookmarks.getItemTitle(id);
   1.256 +          break;
   1.257 +        case PlacesUtils.bookmarks.TYPE_SEPARATOR:
   1.258 +          key = "s" + PlacesUtils.bookmarks.getItemIndex(id);
   1.259 +          break;
   1.260 +        default:
   1.261 +          continue;
   1.262 +      }
   1.263 +
   1.264 +      // The mapping is on a per parent-folder-name basis.
   1.265 +      let parent = PlacesUtils.bookmarks.getFolderIdForItem(id);
   1.266 +      if (parent <= 0)
   1.267 +        continue;
   1.268 +
   1.269 +      let parentName = PlacesUtils.bookmarks.getItemTitle(parent);
   1.270 +      if (guidMap[parentName] == null)
   1.271 +        guidMap[parentName] = {};
   1.272 +
   1.273 +      // If the entry already exists, remember that there are explicit dupes.
   1.274 +      let entry = new String(guid);
   1.275 +      entry.hasDupe = guidMap[parentName][key] != null;
   1.276 +
   1.277 +      // Remember this item's GUID for its parent-name/key pair.
   1.278 +      guidMap[parentName][key] = entry;
   1.279 +      this._log.trace("Mapped: " + [parentName, key, entry, entry.hasDupe]);
   1.280 +    }
   1.281 +
   1.282 +    return guidMap;
   1.283 +  },
   1.284 +
   1.285 +  // Helper function to get a dupe GUID for an item.
   1.286 +  _mapDupe: function _mapDupe(item) {
   1.287 +    // Figure out if we have something to key with.
   1.288 +    let key;
   1.289 +    let altKey;
   1.290 +    switch (item.type) {
   1.291 +      case "query":
   1.292 +        // Prior to Bug 610501, records didn't carry their Smart Bookmark
   1.293 +        // anno, so we won't be able to dupe them correctly. This altKey
   1.294 +        // hack should get them to dupe correctly.
   1.295 +        if (item.queryId) {
   1.296 +          key = "q" + item.queryId;
   1.297 +          altKey = "b" + item.bmkUri + ":" + item.title;
   1.298 +          break;
   1.299 +        }
   1.300 +        // No queryID? Fall through to the regular bookmark case.
   1.301 +      case "bookmark":
   1.302 +      case "microsummary":
   1.303 +        key = "b" + item.bmkUri + ":" + item.title;
   1.304 +        break;
   1.305 +      case "folder":
   1.306 +      case "livemark":
   1.307 +        key = "f" + item.title;
   1.308 +        break;
   1.309 +      case "separator":
   1.310 +        key = "s" + item.pos;
   1.311 +        break;
   1.312 +      default:
   1.313 +        return;
   1.314 +    }
   1.315 +
   1.316 +    // Figure out if we have a map to use!
   1.317 +    // This will throw in some circumstances. That's fine.
   1.318 +    let guidMap = this._guidMap;
   1.319 +
   1.320 +    // Give the GUID if we have the matching pair.
   1.321 +    this._log.trace("Finding mapping: " + item.parentName + ", " + key);
   1.322 +    let parent = guidMap[item.parentName];
   1.323 +    
   1.324 +    if (!parent) {
   1.325 +      this._log.trace("No parent => no dupe.");
   1.326 +      return undefined;
   1.327 +    }
   1.328 +      
   1.329 +    let dupe = parent[key];
   1.330 +    
   1.331 +    if (dupe) {
   1.332 +      this._log.trace("Mapped dupe: " + dupe);
   1.333 +      return dupe;
   1.334 +    }
   1.335 +    
   1.336 +    if (altKey) {
   1.337 +      dupe = parent[altKey];
   1.338 +      if (dupe) {
   1.339 +        this._log.trace("Mapped dupe using altKey " + altKey + ": " + dupe);
   1.340 +        return dupe;
   1.341 +      }
   1.342 +    }
   1.343 +    
   1.344 +    this._log.trace("No dupe found for key " + key + "/" + altKey + ".");
   1.345 +    return undefined;
   1.346 +  },
   1.347 +
   1.348 +  _syncStartup: function _syncStart() {
   1.349 +    SyncEngine.prototype._syncStartup.call(this);
   1.350 +
   1.351 +    let cb = Async.makeSpinningCallback();
   1.352 +    Task.spawn(function() {
   1.353 +      // For first-syncs, make a backup for the user to restore
   1.354 +      if (this.lastSync == 0) {
   1.355 +        this._log.debug("Bookmarks backup starting.");
   1.356 +        yield PlacesBackups.create(null, true);
   1.357 +        this._log.debug("Bookmarks backup done.");
   1.358 +      }
   1.359 +    }.bind(this)).then(
   1.360 +      cb, ex => {
   1.361 +        // Failure to create a backup is somewhat bad, but probably not bad
   1.362 +        // enough to prevent syncing of bookmarks - so just log the error and
   1.363 +        // continue.
   1.364 +        this._log.warn("Got exception \"" + Utils.exceptionStr(ex) +
   1.365 +                       "\" backing up bookmarks, but continuing with sync.");
   1.366 +        cb();
   1.367 +      }
   1.368 +    );
   1.369 +
   1.370 +    cb.wait();
   1.371 +
   1.372 +    this.__defineGetter__("_guidMap", function() {
   1.373 +      // Create a mapping of folder titles and separator positions to GUID.
   1.374 +      // We do this lazily so that we don't do any work unless we reconcile
   1.375 +      // incoming items.
   1.376 +      let guidMap;
   1.377 +      try {
   1.378 +        guidMap = this._buildGUIDMap();
   1.379 +      } catch (ex) {
   1.380 +        this._log.warn("Got exception \"" + Utils.exceptionStr(ex) +
   1.381 +                       "\" building GUID map." +
   1.382 +                       " Skipping all other incoming items.");
   1.383 +        throw {code: Engine.prototype.eEngineAbortApplyIncoming,
   1.384 +               cause: ex};
   1.385 +      }
   1.386 +      delete this._guidMap;
   1.387 +      return this._guidMap = guidMap;
   1.388 +    });
   1.389 +
   1.390 +    this._store._childrenToOrder = {};
   1.391 +  },
   1.392 +
   1.393 +  _processIncoming: function (newitems) {
   1.394 +    try {
   1.395 +      SyncEngine.prototype._processIncoming.call(this, newitems);
   1.396 +    } finally {
   1.397 +      // Reorder children.
   1.398 +      this._tracker.ignoreAll = true;
   1.399 +      this._store._orderChildren();
   1.400 +      this._tracker.ignoreAll = false;
   1.401 +      delete this._store._childrenToOrder;
   1.402 +    }
   1.403 +  },
   1.404 +
   1.405 +  _syncFinish: function _syncFinish() {
   1.406 +    SyncEngine.prototype._syncFinish.call(this);
   1.407 +    this._tracker._ensureMobileQuery();
   1.408 +  },
   1.409 +
   1.410 +  _syncCleanup: function _syncCleanup() {
   1.411 +    SyncEngine.prototype._syncCleanup.call(this);
   1.412 +    delete this._guidMap;
   1.413 +  },
   1.414 +
   1.415 +  _createRecord: function _createRecord(id) {
   1.416 +    // Create the record as usual, but mark it as having dupes if necessary.
   1.417 +    let record = SyncEngine.prototype._createRecord.call(this, id);
   1.418 +    let entry = this._mapDupe(record);
   1.419 +    if (entry != null && entry.hasDupe) {
   1.420 +      record.hasDupe = true;
   1.421 +    }
   1.422 +    return record;
   1.423 +  },
   1.424 +
   1.425 +  _findDupe: function _findDupe(item) {
   1.426 +    this._log.trace("Finding dupe for " + item.id +
   1.427 +                    " (already duped: " + item.hasDupe + ").");
   1.428 +
   1.429 +    // Don't bother finding a dupe if the incoming item has duplicates.
   1.430 +    if (item.hasDupe) {
   1.431 +      this._log.trace(item.id + " already a dupe: not finding one.");
   1.432 +      return;
   1.433 +    }
   1.434 +    let mapped = this._mapDupe(item);
   1.435 +    this._log.debug(item.id + " mapped to " + mapped);
   1.436 +    return mapped;
   1.437 +  }
   1.438 +};
   1.439 +
   1.440 +function BookmarksStore(name, engine) {
   1.441 +  Store.call(this, name, engine);
   1.442 +
   1.443 +  // Explicitly nullify our references to our cached services so we don't leak
   1.444 +  Svc.Obs.add("places-shutdown", function() {
   1.445 +    for each (let [query, stmt] in Iterator(this._stmts)) {
   1.446 +      stmt.finalize();
   1.447 +    }
   1.448 +    this._stmts = {};
   1.449 +  }, this);
   1.450 +}
   1.451 +BookmarksStore.prototype = {
   1.452 +  __proto__: Store.prototype,
   1.453 +
   1.454 +  itemExists: function BStore_itemExists(id) {
   1.455 +    return this.idForGUID(id, true) > 0;
   1.456 +  },
   1.457 +  
   1.458 +  /*
   1.459 +   * If the record is a tag query, rewrite it to refer to the local tag ID.
   1.460 +   * 
   1.461 +   * Otherwise, just return.
   1.462 +   */
   1.463 +  preprocessTagQuery: function preprocessTagQuery(record) {
   1.464 +    if (record.type != "query" ||
   1.465 +        record.bmkUri == null ||
   1.466 +        !record.folderName)
   1.467 +      return;
   1.468 +    
   1.469 +    // Yes, this works without chopping off the "place:" prefix.
   1.470 +    let uri           = record.bmkUri
   1.471 +    let queriesRef    = {};
   1.472 +    let queryCountRef = {};
   1.473 +    let optionsRef    = {};
   1.474 +    PlacesUtils.history.queryStringToQueries(uri, queriesRef, queryCountRef,
   1.475 +                                             optionsRef);
   1.476 +    
   1.477 +    // We only process tag URIs.
   1.478 +    if (optionsRef.value.resultType != optionsRef.value.RESULTS_AS_TAG_CONTENTS)
   1.479 +      return;
   1.480 +    
   1.481 +    // Tag something to ensure that the tag exists.
   1.482 +    let tag = record.folderName;
   1.483 +    let dummyURI = Utils.makeURI("about:weave#BStore_preprocess");
   1.484 +    PlacesUtils.tagging.tagURI(dummyURI, [tag]);
   1.485 +
   1.486 +    // Look for the id of the tag, which might just have been added.
   1.487 +    let tags = this._getNode(PlacesUtils.tagsFolderId);
   1.488 +    if (!(tags instanceof Ci.nsINavHistoryQueryResultNode)) {
   1.489 +      this._log.debug("tags isn't an nsINavHistoryQueryResultNode; aborting.");
   1.490 +      return;
   1.491 +    }
   1.492 +
   1.493 +    tags.containerOpen = true;
   1.494 +    try {
   1.495 +      for (let i = 0; i < tags.childCount; i++) {
   1.496 +        let child = tags.getChild(i);
   1.497 +        if (child.title == tag) {
   1.498 +          // Found the tag, so fix up the query to use the right id.
   1.499 +          this._log.debug("Tag query folder: " + tag + " = " + child.itemId);
   1.500 +          
   1.501 +          this._log.trace("Replacing folders in: " + uri);
   1.502 +          for each (let q in queriesRef.value)
   1.503 +            q.setFolders([child.itemId], 1);
   1.504 +          
   1.505 +          record.bmkUri = PlacesUtils.history.queriesToQueryString(
   1.506 +            queriesRef.value, queryCountRef.value, optionsRef.value);
   1.507 +          return;
   1.508 +        }
   1.509 +      }
   1.510 +    }
   1.511 +    finally {
   1.512 +      tags.containerOpen = false;
   1.513 +    }
   1.514 +  },
   1.515 +  
   1.516 +  applyIncoming: function BStore_applyIncoming(record) {
   1.517 +    this._log.debug("Applying record " + record.id);
   1.518 +    let isSpecial = record.id in kSpecialIds;
   1.519 +
   1.520 +    if (record.deleted) {
   1.521 +      if (isSpecial) {
   1.522 +        this._log.warn("Ignoring deletion for special record " + record.id);
   1.523 +        return;
   1.524 +      }
   1.525 +
   1.526 +      // Don't bother with pre and post-processing for deletions.
   1.527 +      Store.prototype.applyIncoming.call(this, record);
   1.528 +      return;
   1.529 +    }
   1.530 +
   1.531 +    // For special folders we're only interested in child ordering.
   1.532 +    if (isSpecial && record.children) {
   1.533 +      this._log.debug("Processing special node: " + record.id);
   1.534 +      // Reorder children later
   1.535 +      this._childrenToOrder[record.id] = record.children;
   1.536 +      return;
   1.537 +    }
   1.538 +
   1.539 +    // Skip malformed records. (Bug 806460.)
   1.540 +    if (record.type == "query" &&
   1.541 +        !record.bmkUri) {
   1.542 +      this._log.warn("Skipping malformed query bookmark: " + record.id);
   1.543 +      return;
   1.544 +    }
   1.545 +
   1.546 +    // Preprocess the record before doing the normal apply.
   1.547 +    this.preprocessTagQuery(record);
   1.548 +
   1.549 +    // Figure out the local id of the parent GUID if available
   1.550 +    let parentGUID = record.parentid;
   1.551 +    if (!parentGUID) {
   1.552 +      throw "Record " + record.id + " has invalid parentid: " + parentGUID;
   1.553 +    }
   1.554 +    this._log.debug("Local parent is " + parentGUID);
   1.555 +
   1.556 +    let parentId = this.idForGUID(parentGUID);
   1.557 +    if (parentId > 0) {
   1.558 +      // Save the parent id for modifying the bookmark later
   1.559 +      record._parent = parentId;
   1.560 +      record._orphan = false;
   1.561 +      this._log.debug("Record " + record.id + " is not an orphan.");
   1.562 +    } else {
   1.563 +      this._log.trace("Record " + record.id +
   1.564 +                      " is an orphan: could not find parent " + parentGUID);
   1.565 +      record._orphan = true;
   1.566 +    }
   1.567 +
   1.568 +    // Do the normal processing of incoming records
   1.569 +    Store.prototype.applyIncoming.call(this, record);
   1.570 +
   1.571 +    // Do some post-processing if we have an item
   1.572 +    let itemId = this.idForGUID(record.id);
   1.573 +    if (itemId > 0) {
   1.574 +      // Move any children that are looking for this folder as a parent
   1.575 +      if (record.type == "folder") {
   1.576 +        this._reparentOrphans(itemId);
   1.577 +        // Reorder children later
   1.578 +        if (record.children)
   1.579 +          this._childrenToOrder[record.id] = record.children;
   1.580 +      }
   1.581 +
   1.582 +      // Create an annotation to remember that it needs reparenting.
   1.583 +      if (record._orphan) {
   1.584 +        PlacesUtils.annotations.setItemAnnotation(
   1.585 +          itemId, PARENT_ANNO, parentGUID, 0,
   1.586 +          PlacesUtils.annotations.EXPIRE_NEVER);
   1.587 +      }
   1.588 +    }
   1.589 +  },
   1.590 +
   1.591 +  /**
   1.592 +   * Find all ids of items that have a given value for an annotation
   1.593 +   */
   1.594 +  _findAnnoItems: function BStore__findAnnoItems(anno, val) {
   1.595 +    return PlacesUtils.annotations.getItemsWithAnnotation(anno, {})
   1.596 +                      .filter(function(id) {
   1.597 +      return PlacesUtils.annotations.getItemAnnotation(id, anno) == val;
   1.598 +    });
   1.599 +  },
   1.600 +
   1.601 +  /**
   1.602 +   * For the provided parent item, attach its children to it
   1.603 +   */
   1.604 +  _reparentOrphans: function _reparentOrphans(parentId) {
   1.605 +    // Find orphans and reunite with this folder parent
   1.606 +    let parentGUID = this.GUIDForId(parentId);
   1.607 +    let orphans = this._findAnnoItems(PARENT_ANNO, parentGUID);
   1.608 +
   1.609 +    this._log.debug("Reparenting orphans " + orphans + " to " + parentId);
   1.610 +    orphans.forEach(function(orphan) {
   1.611 +      // Move the orphan to the parent and drop the missing parent annotation
   1.612 +      if (this._reparentItem(orphan, parentId)) {
   1.613 +        PlacesUtils.annotations.removeItemAnnotation(orphan, PARENT_ANNO);
   1.614 +      }
   1.615 +    }, this);
   1.616 +  },
   1.617 +
   1.618 +  _reparentItem: function _reparentItem(itemId, parentId) {
   1.619 +    this._log.trace("Attempting to move item " + itemId + " to new parent " +
   1.620 +                    parentId);
   1.621 +    try {
   1.622 +      if (parentId > 0) {
   1.623 +        PlacesUtils.bookmarks.moveItem(itemId, parentId,
   1.624 +                                       PlacesUtils.bookmarks.DEFAULT_INDEX);
   1.625 +        return true;
   1.626 +      }
   1.627 +    } catch(ex) {
   1.628 +      this._log.debug("Failed to reparent item. " + Utils.exceptionStr(ex));
   1.629 +    }
   1.630 +    return false;
   1.631 +  },
   1.632 +
   1.633 +  // Turn a record's nsINavBookmarksService constant and other attributes into
   1.634 +  // a granular type for comparison.
   1.635 +  _recordType: function _recordType(itemId) {
   1.636 +    let bms  = PlacesUtils.bookmarks;
   1.637 +    let type = bms.getItemType(itemId);
   1.638 +
   1.639 +    switch (type) {
   1.640 +      case bms.TYPE_FOLDER:
   1.641 +        if (PlacesUtils.annotations
   1.642 +                       .itemHasAnnotation(itemId, PlacesUtils.LMANNO_FEEDURI)) {
   1.643 +          return "livemark";
   1.644 +        }
   1.645 +        return "folder";
   1.646 +
   1.647 +      case bms.TYPE_BOOKMARK:
   1.648 +        let bmkUri = bms.getBookmarkURI(itemId).spec;
   1.649 +        if (bmkUri.indexOf("place:") == 0) {
   1.650 +          return "query";
   1.651 +        }
   1.652 +        return "bookmark";
   1.653 +
   1.654 +      case bms.TYPE_SEPARATOR:
   1.655 +        return "separator";
   1.656 +
   1.657 +      default:
   1.658 +        return null;
   1.659 +    }
   1.660 +  },
   1.661 +
   1.662 +  create: function BStore_create(record) {
   1.663 +    // Default to unfiled if we don't have the parent yet.
   1.664 +    
   1.665 +    // Valid parent IDs are all positive integers. Other values -- undefined,
   1.666 +    // null, -1 -- all compare false for > 0, so this catches them all. We
   1.667 +    // don't just use <= without the !, because undefined and null compare
   1.668 +    // false for that, too!
   1.669 +    if (!(record._parent > 0)) {
   1.670 +      this._log.debug("Parent is " + record._parent + "; reparenting to unfiled.");
   1.671 +      record._parent = kSpecialIds.unfiled;
   1.672 +    }
   1.673 +
   1.674 +    let newId;
   1.675 +    switch (record.type) {
   1.676 +    case "bookmark":
   1.677 +    case "query":
   1.678 +    case "microsummary": {
   1.679 +      let uri = Utils.makeURI(record.bmkUri);
   1.680 +      newId = PlacesUtils.bookmarks.insertBookmark(
   1.681 +        record._parent, uri, PlacesUtils.bookmarks.DEFAULT_INDEX, record.title);
   1.682 +      this._log.debug("created bookmark " + newId + " under " + record._parent
   1.683 +                      + " as " + record.title + " " + record.bmkUri);
   1.684 +
   1.685 +      // Smart bookmark annotations are strings.
   1.686 +      if (record.queryId) {
   1.687 +        PlacesUtils.annotations.setItemAnnotation(
   1.688 +          newId, SMART_BOOKMARKS_ANNO, record.queryId, 0,
   1.689 +          PlacesUtils.annotations.EXPIRE_NEVER);
   1.690 +      }
   1.691 +
   1.692 +      if (Array.isArray(record.tags)) {
   1.693 +        this._tagURI(uri, record.tags);
   1.694 +      }
   1.695 +      PlacesUtils.bookmarks.setKeywordForBookmark(newId, record.keyword);
   1.696 +      if (record.description) {
   1.697 +        PlacesUtils.annotations.setItemAnnotation(
   1.698 +          newId, DESCRIPTION_ANNO, record.description, 0,
   1.699 +          PlacesUtils.annotations.EXPIRE_NEVER);
   1.700 +      }
   1.701 +
   1.702 +      if (record.loadInSidebar) {
   1.703 +        PlacesUtils.annotations.setItemAnnotation(
   1.704 +          newId, SIDEBAR_ANNO, true, 0,
   1.705 +          PlacesUtils.annotations.EXPIRE_NEVER);
   1.706 +      }
   1.707 +
   1.708 +    } break;
   1.709 +    case "folder":
   1.710 +      newId = PlacesUtils.bookmarks.createFolder(
   1.711 +        record._parent, record.title, PlacesUtils.bookmarks.DEFAULT_INDEX);
   1.712 +      this._log.debug("created folder " + newId + " under " + record._parent
   1.713 +                      + " as " + record.title);
   1.714 +
   1.715 +      if (record.description) {
   1.716 +        PlacesUtils.annotations.setItemAnnotation(
   1.717 +          newId, DESCRIPTION_ANNO, record.description, 0,
   1.718 +          PlacesUtils.annotations.EXPIRE_NEVER);
   1.719 +      }
   1.720 +
   1.721 +      // record.children will be dealt with in _orderChildren.
   1.722 +      break;
   1.723 +    case "livemark":
   1.724 +      let siteURI = null;
   1.725 +      if (!record.feedUri) {
   1.726 +        this._log.debug("No feed URI: skipping livemark record " + record.id);
   1.727 +        return;
   1.728 +      }
   1.729 +      if (PlacesUtils.annotations
   1.730 +                     .itemHasAnnotation(record._parent, PlacesUtils.LMANNO_FEEDURI)) {
   1.731 +        this._log.debug("Invalid parent: skipping livemark record " + record.id);
   1.732 +        return;
   1.733 +      }
   1.734 +
   1.735 +      if (record.siteUri != null)
   1.736 +        siteURI = Utils.makeURI(record.siteUri);
   1.737 +
   1.738 +      // Until this engine can handle asynchronous error reporting, we need to
   1.739 +      // detect errors on creation synchronously.
   1.740 +      let spinningCb = Async.makeSpinningCallback();
   1.741 +
   1.742 +      let livemarkObj = {title: record.title,
   1.743 +                         parentId: record._parent,
   1.744 +                         index: PlacesUtils.bookmarks.DEFAULT_INDEX,
   1.745 +                         feedURI: Utils.makeURI(record.feedUri),
   1.746 +                         siteURI: siteURI,
   1.747 +                         guid: record.id};
   1.748 +      PlacesUtils.livemarks.addLivemark(livemarkObj).then(
   1.749 +        aLivemark => { spinningCb(null, [Components.results.NS_OK, aLivemark]) },
   1.750 +        () => { spinningCb(null, [Components.results.NS_ERROR_UNEXPECTED, aLivemark]) }
   1.751 +      );
   1.752 +
   1.753 +      let [status, livemark] = spinningCb.wait();
   1.754 +      if (!Components.isSuccessCode(status)) {
   1.755 +        throw status;
   1.756 +      }
   1.757 +
   1.758 +      this._log.debug("Created livemark " + livemark.id + " under " +
   1.759 +                      livemark.parentId + " as " + livemark.title +
   1.760 +                      ", " + livemark.siteURI.spec + ", " +
   1.761 +                      livemark.feedURI.spec + ", GUID " +
   1.762 +                      livemark.guid);
   1.763 +      break;
   1.764 +    case "separator":
   1.765 +      newId = PlacesUtils.bookmarks.insertSeparator(
   1.766 +        record._parent, PlacesUtils.bookmarks.DEFAULT_INDEX);
   1.767 +      this._log.debug("created separator " + newId + " under " + record._parent);
   1.768 +      break;
   1.769 +    case "item":
   1.770 +      this._log.debug(" -> got a generic places item.. do nothing?");
   1.771 +      return;
   1.772 +    default:
   1.773 +      this._log.error("_create: Unknown item type: " + record.type);
   1.774 +      return;
   1.775 +    }
   1.776 +
   1.777 +    if (newId) {
   1.778 +      // Livemarks can set the GUID through the API, so there's no need to
   1.779 +      // do that here.
   1.780 +      this._log.trace("Setting GUID of new item " + newId + " to " + record.id);
   1.781 +      this._setGUID(newId, record.id);
   1.782 +    }
   1.783 +  },
   1.784 +
   1.785 +  // Factored out of `remove` to avoid redundant DB queries when the Places ID
   1.786 +  // is already known.
   1.787 +  removeById: function removeById(itemId, guid) {
   1.788 +    let type = PlacesUtils.bookmarks.getItemType(itemId);
   1.789 +
   1.790 +    switch (type) {
   1.791 +    case PlacesUtils.bookmarks.TYPE_BOOKMARK:
   1.792 +      this._log.debug("  -> removing bookmark " + guid);
   1.793 +      PlacesUtils.bookmarks.removeItem(itemId);
   1.794 +      break;
   1.795 +    case PlacesUtils.bookmarks.TYPE_FOLDER:
   1.796 +      this._log.debug("  -> removing folder " + guid);
   1.797 +      PlacesUtils.bookmarks.removeItem(itemId);
   1.798 +      break;
   1.799 +    case PlacesUtils.bookmarks.TYPE_SEPARATOR:
   1.800 +      this._log.debug("  -> removing separator " + guid);
   1.801 +      PlacesUtils.bookmarks.removeItem(itemId);
   1.802 +      break;
   1.803 +    default:
   1.804 +      this._log.error("remove: Unknown item type: " + type);
   1.805 +      break;
   1.806 +    }
   1.807 +  },
   1.808 +
   1.809 +  remove: function BStore_remove(record) {
   1.810 +    if (kSpecialIds.isSpecialGUID(record.id)) {
   1.811 +      this._log.warn("Refusing to remove special folder " + record.id);
   1.812 +      return;
   1.813 +    }
   1.814 +
   1.815 +    let itemId = this.idForGUID(record.id);
   1.816 +    if (itemId <= 0) {
   1.817 +      this._log.debug("Item " + record.id + " already removed");
   1.818 +      return;
   1.819 +    }
   1.820 +    this.removeById(itemId, record.id);
   1.821 +  },
   1.822 +
   1.823 +  _taggableTypes: ["bookmark", "microsummary", "query"],
   1.824 +  isTaggable: function isTaggable(recordType) {
   1.825 +    return this._taggableTypes.indexOf(recordType) != -1;
   1.826 +  },
   1.827 +
   1.828 +  update: function BStore_update(record) {
   1.829 +    let itemId = this.idForGUID(record.id);
   1.830 +
   1.831 +    if (itemId <= 0) {
   1.832 +      this._log.debug("Skipping update for unknown item: " + record.id);
   1.833 +      return;
   1.834 +    }
   1.835 +
   1.836 +    // Two items are the same type if they have the same ItemType in Places,
   1.837 +    // and also share some key characteristics (e.g., both being livemarks).
   1.838 +    // We figure this out by examining the item to find the equivalent granular
   1.839 +    // (string) type.
   1.840 +    // If they're not the same type, we can't just update attributes. Delete
   1.841 +    // then recreate the record instead.
   1.842 +    let localItemType    = this._recordType(itemId);
   1.843 +    let remoteRecordType = record.type;
   1.844 +    this._log.trace("Local type: " + localItemType + ". " +
   1.845 +                    "Remote type: " + remoteRecordType + ".");
   1.846 +
   1.847 +    if (localItemType != remoteRecordType) {
   1.848 +      this._log.debug("Local record and remote record differ in type. " +
   1.849 +                      "Deleting and recreating.");
   1.850 +      this.removeById(itemId, record.id);
   1.851 +      this.create(record);
   1.852 +      return;
   1.853 +    }
   1.854 +
   1.855 +    this._log.trace("Updating " + record.id + " (" + itemId + ")");
   1.856 +
   1.857 +    // Move the bookmark to a new parent or new position if necessary
   1.858 +    if (record._parent > 0 &&
   1.859 +        PlacesUtils.bookmarks.getFolderIdForItem(itemId) != record._parent) {
   1.860 +      this._reparentItem(itemId, record._parent);
   1.861 +    }
   1.862 +
   1.863 +    for (let [key, val] in Iterator(record.cleartext)) {
   1.864 +      switch (key) {
   1.865 +      case "title":
   1.866 +        PlacesUtils.bookmarks.setItemTitle(itemId, val);
   1.867 +        break;
   1.868 +      case "bmkUri":
   1.869 +        PlacesUtils.bookmarks.changeBookmarkURI(itemId, Utils.makeURI(val));
   1.870 +        break;
   1.871 +      case "tags":
   1.872 +        if (Array.isArray(val)) {
   1.873 +          if (this.isTaggable(remoteRecordType)) {
   1.874 +            this._tagID(itemId, val);
   1.875 +          } else {
   1.876 +            this._log.debug("Remote record type is invalid for tags: " + remoteRecordType);
   1.877 +          }
   1.878 +        }
   1.879 +        break;
   1.880 +      case "keyword":
   1.881 +        PlacesUtils.bookmarks.setKeywordForBookmark(itemId, val);
   1.882 +        break;
   1.883 +      case "description":
   1.884 +        if (val) {
   1.885 +          PlacesUtils.annotations.setItemAnnotation(
   1.886 +            itemId, DESCRIPTION_ANNO, val, 0,
   1.887 +            PlacesUtils.annotations.EXPIRE_NEVER);
   1.888 +        } else {
   1.889 +          PlacesUtils.annotations.removeItemAnnotation(itemId, DESCRIPTION_ANNO);
   1.890 +        }
   1.891 +        break;
   1.892 +      case "loadInSidebar":
   1.893 +        if (val) {
   1.894 +          PlacesUtils.annotations.setItemAnnotation(
   1.895 +            itemId, SIDEBAR_ANNO, true, 0,
   1.896 +            PlacesUtils.annotations.EXPIRE_NEVER);
   1.897 +        } else {
   1.898 +          PlacesUtils.annotations.removeItemAnnotation(itemId, SIDEBAR_ANNO);
   1.899 +        }
   1.900 +        break;
   1.901 +      case "queryId":
   1.902 +        PlacesUtils.annotations.setItemAnnotation(
   1.903 +          itemId, SMART_BOOKMARKS_ANNO, val, 0,
   1.904 +          PlacesUtils.annotations.EXPIRE_NEVER);
   1.905 +        break;
   1.906 +      }
   1.907 +    }
   1.908 +  },
   1.909 +
   1.910 +  _orderChildren: function _orderChildren() {
   1.911 +    for (let [guid, children] in Iterator(this._childrenToOrder)) {
   1.912 +      // Reorder children according to the GUID list. Gracefully deal
   1.913 +      // with missing items, e.g. locally deleted.
   1.914 +      let delta = 0;
   1.915 +      let parent = null;
   1.916 +      for (let idx = 0; idx < children.length; idx++) {
   1.917 +        let itemid = this.idForGUID(children[idx]);
   1.918 +        if (itemid == -1) {
   1.919 +          delta += 1;
   1.920 +          this._log.trace("Could not locate record " + children[idx]);
   1.921 +          continue;
   1.922 +        }
   1.923 +        try {
   1.924 +          // This code path could be optimized by caching the parent earlier.
   1.925 +          // Doing so should take in count any edge case due to reparenting
   1.926 +          // or parent invalidations though.
   1.927 +          if (!parent) {
   1.928 +            parent = PlacesUtils.bookmarks.getFolderIdForItem(itemid);
   1.929 +          }
   1.930 +          PlacesUtils.bookmarks.moveItem(itemid, parent, idx - delta);
   1.931 +        } catch (ex) {
   1.932 +          this._log.debug("Could not move item " + children[idx] + ": " + ex);
   1.933 +        }
   1.934 +      }
   1.935 +    }
   1.936 +  },
   1.937 +
   1.938 +  changeItemID: function BStore_changeItemID(oldID, newID) {
   1.939 +    this._log.debug("Changing GUID " + oldID + " to " + newID);
   1.940 +
   1.941 +    // Make sure there's an item to change GUIDs
   1.942 +    let itemId = this.idForGUID(oldID);
   1.943 +    if (itemId <= 0)
   1.944 +      return;
   1.945 +
   1.946 +    this._setGUID(itemId, newID);
   1.947 +  },
   1.948 +
   1.949 +  _getNode: function BStore__getNode(folder) {
   1.950 +    let query = PlacesUtils.history.getNewQuery();
   1.951 +    query.setFolders([folder], 1);
   1.952 +    return PlacesUtils.history.executeQuery(
   1.953 +      query, PlacesUtils.history.getNewQueryOptions()).root;
   1.954 +  },
   1.955 +
   1.956 +  _getTags: function BStore__getTags(uri) {
   1.957 +    try {
   1.958 +      if (typeof(uri) == "string")
   1.959 +        uri = Utils.makeURI(uri);
   1.960 +    } catch(e) {
   1.961 +      this._log.warn("Could not parse URI \"" + uri + "\": " + e);
   1.962 +    }
   1.963 +    return PlacesUtils.tagging.getTagsForURI(uri, {});
   1.964 +  },
   1.965 +
   1.966 +  _getDescription: function BStore__getDescription(id) {
   1.967 +    try {
   1.968 +      return PlacesUtils.annotations.getItemAnnotation(id, DESCRIPTION_ANNO);
   1.969 +    } catch (e) {
   1.970 +      return null;
   1.971 +    }
   1.972 +  },
   1.973 +
   1.974 +  _isLoadInSidebar: function BStore__isLoadInSidebar(id) {
   1.975 +    return PlacesUtils.annotations.itemHasAnnotation(id, SIDEBAR_ANNO);
   1.976 +  },
   1.977 +
   1.978 +  get _childGUIDsStm() {
   1.979 +    return this._getStmt(
   1.980 +      "SELECT id AS item_id, guid " +
   1.981 +      "FROM moz_bookmarks " +
   1.982 +      "WHERE parent = :parent " +
   1.983 +      "ORDER BY position");
   1.984 +  },
   1.985 +  _childGUIDsCols: ["item_id", "guid"],
   1.986 +
   1.987 +  _getChildGUIDsForId: function _getChildGUIDsForId(itemid) {
   1.988 +    let stmt = this._childGUIDsStm;
   1.989 +    stmt.params.parent = itemid;
   1.990 +    let rows = Async.querySpinningly(stmt, this._childGUIDsCols);
   1.991 +    return rows.map(function (row) {
   1.992 +      if (row.guid) {
   1.993 +        return row.guid;
   1.994 +      }
   1.995 +      // A GUID hasn't been assigned to this item yet, do this now.
   1.996 +      return this.GUIDForId(row.item_id);
   1.997 +    }, this);
   1.998 +  },
   1.999 +
  1.1000 +  // Create a record starting from the weave id (places guid)
  1.1001 +  createRecord: function createRecord(id, collection) {
  1.1002 +    let placeId = this.idForGUID(id);
  1.1003 +    let record;
  1.1004 +    if (placeId <= 0) { // deleted item
  1.1005 +      record = new PlacesItem(collection, id);
  1.1006 +      record.deleted = true;
  1.1007 +      return record;
  1.1008 +    }
  1.1009 +
  1.1010 +    let parent = PlacesUtils.bookmarks.getFolderIdForItem(placeId);
  1.1011 +    switch (PlacesUtils.bookmarks.getItemType(placeId)) {
  1.1012 +    case PlacesUtils.bookmarks.TYPE_BOOKMARK:
  1.1013 +      let bmkUri = PlacesUtils.bookmarks.getBookmarkURI(placeId).spec;
  1.1014 +      if (bmkUri.indexOf("place:") == 0) {
  1.1015 +        record = new BookmarkQuery(collection, id);
  1.1016 +
  1.1017 +        // Get the actual tag name instead of the local itemId
  1.1018 +        let folder = bmkUri.match(/[:&]folder=(\d+)/);
  1.1019 +        try {
  1.1020 +          // There might not be the tag yet when creating on a new client
  1.1021 +          if (folder != null) {
  1.1022 +            folder = folder[1];
  1.1023 +            record.folderName = PlacesUtils.bookmarks.getItemTitle(folder);
  1.1024 +            this._log.trace("query id: " + folder + " = " + record.folderName);
  1.1025 +          }
  1.1026 +        }
  1.1027 +        catch(ex) {}
  1.1028 +        
  1.1029 +        // Persist the Smart Bookmark anno, if found.
  1.1030 +        try {
  1.1031 +          let anno = PlacesUtils.annotations.getItemAnnotation(placeId, SMART_BOOKMARKS_ANNO);
  1.1032 +          if (anno != null) {
  1.1033 +            this._log.trace("query anno: " + SMART_BOOKMARKS_ANNO +
  1.1034 +                            " = " + anno);
  1.1035 +            record.queryId = anno;
  1.1036 +          }
  1.1037 +        }
  1.1038 +        catch(ex) {}
  1.1039 +      }
  1.1040 +      else {
  1.1041 +        record = new Bookmark(collection, id);
  1.1042 +      }
  1.1043 +      record.title = PlacesUtils.bookmarks.getItemTitle(placeId);
  1.1044 +
  1.1045 +      record.parentName = PlacesUtils.bookmarks.getItemTitle(parent);
  1.1046 +      record.bmkUri = bmkUri;
  1.1047 +      record.tags = this._getTags(record.bmkUri);
  1.1048 +      record.keyword = PlacesUtils.bookmarks.getKeywordForBookmark(placeId);
  1.1049 +      record.description = this._getDescription(placeId);
  1.1050 +      record.loadInSidebar = this._isLoadInSidebar(placeId);
  1.1051 +      break;
  1.1052 +
  1.1053 +    case PlacesUtils.bookmarks.TYPE_FOLDER:
  1.1054 +      if (PlacesUtils.annotations
  1.1055 +                     .itemHasAnnotation(placeId, PlacesUtils.LMANNO_FEEDURI)) {
  1.1056 +        record = new Livemark(collection, id);
  1.1057 +        let as = PlacesUtils.annotations;
  1.1058 +        record.feedUri = as.getItemAnnotation(placeId, PlacesUtils.LMANNO_FEEDURI);
  1.1059 +        try {
  1.1060 +          record.siteUri = as.getItemAnnotation(placeId, PlacesUtils.LMANNO_SITEURI);
  1.1061 +        } catch (ex) {}
  1.1062 +      } else {
  1.1063 +        record = new BookmarkFolder(collection, id);
  1.1064 +      }
  1.1065 +
  1.1066 +      if (parent > 0)
  1.1067 +        record.parentName = PlacesUtils.bookmarks.getItemTitle(parent);
  1.1068 +      record.title = PlacesUtils.bookmarks.getItemTitle(placeId);
  1.1069 +      record.description = this._getDescription(placeId);
  1.1070 +      record.children = this._getChildGUIDsForId(placeId);
  1.1071 +      break;
  1.1072 +
  1.1073 +    case PlacesUtils.bookmarks.TYPE_SEPARATOR:
  1.1074 +      record = new BookmarkSeparator(collection, id);
  1.1075 +      if (parent > 0)
  1.1076 +        record.parentName = PlacesUtils.bookmarks.getItemTitle(parent);
  1.1077 +      // Create a positioning identifier for the separator, used by _mapDupe
  1.1078 +      record.pos = PlacesUtils.bookmarks.getItemIndex(placeId);
  1.1079 +      break;
  1.1080 +
  1.1081 +    default:
  1.1082 +      record = new PlacesItem(collection, id);
  1.1083 +      this._log.warn("Unknown item type, cannot serialize: " +
  1.1084 +                     PlacesUtils.bookmarks.getItemType(placeId));
  1.1085 +    }
  1.1086 +
  1.1087 +    record.parentid = this.GUIDForId(parent);
  1.1088 +    record.sortindex = this._calculateIndex(record);
  1.1089 +
  1.1090 +    return record;
  1.1091 +  },
  1.1092 +
  1.1093 +  _stmts: {},
  1.1094 +  _getStmt: function(query) {
  1.1095 +    if (query in this._stmts) {
  1.1096 +      return this._stmts[query];
  1.1097 +    }
  1.1098 +
  1.1099 +    this._log.trace("Creating SQL statement: " + query);
  1.1100 +    let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
  1.1101 +                        .DBConnection;
  1.1102 +    return this._stmts[query] = db.createAsyncStatement(query);
  1.1103 +  },
  1.1104 +
  1.1105 +  get _frecencyStm() {
  1.1106 +    return this._getStmt(
  1.1107 +        "SELECT frecency " +
  1.1108 +        "FROM moz_places " +
  1.1109 +        "WHERE url = :url " +
  1.1110 +        "LIMIT 1");
  1.1111 +  },
  1.1112 +  _frecencyCols: ["frecency"],
  1.1113 +
  1.1114 +  get _setGUIDStm() {
  1.1115 +    return this._getStmt(
  1.1116 +      "UPDATE moz_bookmarks " +
  1.1117 +      "SET guid = :guid " +
  1.1118 +      "WHERE id = :item_id");
  1.1119 +  },
  1.1120 +
  1.1121 +  // Some helper functions to handle GUIDs
  1.1122 +  _setGUID: function _setGUID(id, guid) {
  1.1123 +    if (!guid)
  1.1124 +      guid = Utils.makeGUID();
  1.1125 +
  1.1126 +    let stmt = this._setGUIDStm;
  1.1127 +    stmt.params.guid = guid;
  1.1128 +    stmt.params.item_id = id;
  1.1129 +    Async.querySpinningly(stmt);
  1.1130 +    return guid;
  1.1131 +  },
  1.1132 +
  1.1133 +  get _guidForIdStm() {
  1.1134 +    return this._getStmt(
  1.1135 +      "SELECT guid " +
  1.1136 +      "FROM moz_bookmarks " +
  1.1137 +      "WHERE id = :item_id");
  1.1138 +  },
  1.1139 +  _guidForIdCols: ["guid"],
  1.1140 +
  1.1141 +  GUIDForId: function GUIDForId(id) {
  1.1142 +    let special = kSpecialIds.specialGUIDForId(id);
  1.1143 +    if (special)
  1.1144 +      return special;
  1.1145 +
  1.1146 +    let stmt = this._guidForIdStm;
  1.1147 +    stmt.params.item_id = id;
  1.1148 +
  1.1149 +    // Use the existing GUID if it exists
  1.1150 +    let result = Async.querySpinningly(stmt, this._guidForIdCols)[0];
  1.1151 +    if (result && result.guid)
  1.1152 +      return result.guid;
  1.1153 +
  1.1154 +    // Give the uri a GUID if it doesn't have one
  1.1155 +    return this._setGUID(id);
  1.1156 +  },
  1.1157 +
  1.1158 +  get _idForGUIDStm() {
  1.1159 +    return this._getStmt(
  1.1160 +      "SELECT id AS item_id " +
  1.1161 +      "FROM moz_bookmarks " +
  1.1162 +      "WHERE guid = :guid");
  1.1163 +  },
  1.1164 +  _idForGUIDCols: ["item_id"],
  1.1165 +
  1.1166 +  // noCreate is provided as an optional argument to prevent the creation of
  1.1167 +  // non-existent special records, such as "mobile".
  1.1168 +  idForGUID: function idForGUID(guid, noCreate) {
  1.1169 +    if (kSpecialIds.isSpecialGUID(guid))
  1.1170 +      return kSpecialIds.specialIdForGUID(guid, !noCreate);
  1.1171 +
  1.1172 +    let stmt = this._idForGUIDStm;
  1.1173 +    // guid might be a String object rather than a string.
  1.1174 +    stmt.params.guid = guid.toString();
  1.1175 +
  1.1176 +    let results = Async.querySpinningly(stmt, this._idForGUIDCols);
  1.1177 +    this._log.trace("Number of rows matching GUID " + guid + ": "
  1.1178 +                    + results.length);
  1.1179 +    
  1.1180 +    // Here's the one we care about: the first.
  1.1181 +    let result = results[0];
  1.1182 +    
  1.1183 +    if (!result)
  1.1184 +      return -1;
  1.1185 +    
  1.1186 +    return result.item_id;
  1.1187 +  },
  1.1188 +
  1.1189 +  _calculateIndex: function _calculateIndex(record) {
  1.1190 +    // Ensure folders have a very high sort index so they're not synced last.
  1.1191 +    if (record.type == "folder")
  1.1192 +      return FOLDER_SORTINDEX;
  1.1193 +
  1.1194 +    // For anything directly under the toolbar, give it a boost of more than an
  1.1195 +    // unvisited bookmark
  1.1196 +    let index = 0;
  1.1197 +    if (record.parentid == "toolbar")
  1.1198 +      index += 150;
  1.1199 +
  1.1200 +    // Add in the bookmark's frecency if we have something.
  1.1201 +    if (record.bmkUri != null) {
  1.1202 +      this._frecencyStm.params.url = record.bmkUri;
  1.1203 +      let result = Async.querySpinningly(this._frecencyStm, this._frecencyCols);
  1.1204 +      if (result.length)
  1.1205 +        index += result[0].frecency;
  1.1206 +    }
  1.1207 +
  1.1208 +    return index;
  1.1209 +  },
  1.1210 +
  1.1211 +  _getChildren: function BStore_getChildren(guid, items) {
  1.1212 +    let node = guid; // the recursion case
  1.1213 +    if (typeof(node) == "string") { // callers will give us the guid as the first arg
  1.1214 +      let nodeID = this.idForGUID(guid, true);
  1.1215 +      if (!nodeID) {
  1.1216 +        this._log.debug("No node for GUID " + guid + "; returning no children.");
  1.1217 +        return items;
  1.1218 +      }
  1.1219 +      node = this._getNode(nodeID);
  1.1220 +    }
  1.1221 +    
  1.1222 +    if (node.type == node.RESULT_TYPE_FOLDER) {
  1.1223 +      node.QueryInterface(Ci.nsINavHistoryQueryResultNode);
  1.1224 +      node.containerOpen = true;
  1.1225 +      try {
  1.1226 +        // Remember all the children GUIDs and recursively get more
  1.1227 +        for (let i = 0; i < node.childCount; i++) {
  1.1228 +          let child = node.getChild(i);
  1.1229 +          items[this.GUIDForId(child.itemId)] = true;
  1.1230 +          this._getChildren(child, items);
  1.1231 +        }
  1.1232 +      }
  1.1233 +      finally {
  1.1234 +        node.containerOpen = false;
  1.1235 +      }
  1.1236 +    }
  1.1237 +
  1.1238 +    return items;
  1.1239 +  },
  1.1240 +
  1.1241 +  /**
  1.1242 +   * Associates the URI of the item with the provided ID with the
  1.1243 +   * provided array of tags.
  1.1244 +   * If the provided ID does not identify an item with a URI,
  1.1245 +   * returns immediately.
  1.1246 +   */
  1.1247 +  _tagID: function _tagID(itemID, tags) {
  1.1248 +    if (!itemID || !tags) {
  1.1249 +      return;
  1.1250 +    }
  1.1251 +
  1.1252 +    try {
  1.1253 +      let u = PlacesUtils.bookmarks.getBookmarkURI(itemID);
  1.1254 +      this._tagURI(u, tags);
  1.1255 +    } catch (e) {
  1.1256 +      this._log.warn("Got exception fetching URI for " + itemID + ": not tagging. " +
  1.1257 +                     Utils.exceptionStr(e));
  1.1258 +
  1.1259 +      // I guess it doesn't have a URI. Don't try to tag it.
  1.1260 +      return;
  1.1261 +    }
  1.1262 +  },
  1.1263 +
  1.1264 +  /**
  1.1265 +   * Associate the provided URI with the provided array of tags.
  1.1266 +   * If the provided URI is falsy, returns immediately.
  1.1267 +   */
  1.1268 +  _tagURI: function _tagURI(bookmarkURI, tags) {
  1.1269 +    if (!bookmarkURI || !tags) {
  1.1270 +      return;
  1.1271 +    }
  1.1272 +
  1.1273 +    // Filter out any null/undefined/empty tags.
  1.1274 +    tags = tags.filter(function(t) t);
  1.1275 +
  1.1276 +    // Temporarily tag a dummy URI to preserve tag ids when untagging.
  1.1277 +    let dummyURI = Utils.makeURI("about:weave#BStore_tagURI");
  1.1278 +    PlacesUtils.tagging.tagURI(dummyURI, tags);
  1.1279 +    PlacesUtils.tagging.untagURI(bookmarkURI, null);
  1.1280 +    PlacesUtils.tagging.tagURI(bookmarkURI, tags);
  1.1281 +    PlacesUtils.tagging.untagURI(dummyURI, null);
  1.1282 +  },
  1.1283 +
  1.1284 +  getAllIDs: function BStore_getAllIDs() {
  1.1285 +    let items = {"menu": true,
  1.1286 +                 "toolbar": true};
  1.1287 +    for each (let guid in kSpecialIds.guids) {
  1.1288 +      if (guid != "places" && guid != "tags")
  1.1289 +        this._getChildren(guid, items);
  1.1290 +    }
  1.1291 +    return items;
  1.1292 +  },
  1.1293 +
  1.1294 +  wipe: function BStore_wipe() {
  1.1295 +    let cb = Async.makeSpinningCallback();
  1.1296 +    Task.spawn(function() {
  1.1297 +      // Save a backup before clearing out all bookmarks.
  1.1298 +      yield PlacesBackups.create(null, true);
  1.1299 +      for each (let guid in kSpecialIds.guids)
  1.1300 +        if (guid != "places") {
  1.1301 +          let id = kSpecialIds.specialIdForGUID(guid);
  1.1302 +          if (id)
  1.1303 +            PlacesUtils.bookmarks.removeFolderChildren(id);
  1.1304 +        }
  1.1305 +      cb();
  1.1306 +    });
  1.1307 +    cb.wait();
  1.1308 +  }
  1.1309 +};
  1.1310 +
  1.1311 +function BookmarksTracker(name, engine) {
  1.1312 +  Tracker.call(this, name, engine);
  1.1313 +
  1.1314 +  Svc.Obs.add("places-shutdown", this);
  1.1315 +}
  1.1316 +BookmarksTracker.prototype = {
  1.1317 +  __proto__: Tracker.prototype,
  1.1318 +
  1.1319 +  startTracking: function() {
  1.1320 +    PlacesUtils.bookmarks.addObserver(this, true);
  1.1321 +    Svc.Obs.add("bookmarks-restore-begin", this);
  1.1322 +    Svc.Obs.add("bookmarks-restore-success", this);
  1.1323 +    Svc.Obs.add("bookmarks-restore-failed", this);
  1.1324 +  },
  1.1325 +
  1.1326 +  stopTracking: function() {
  1.1327 +    PlacesUtils.bookmarks.removeObserver(this);
  1.1328 +    Svc.Obs.remove("bookmarks-restore-begin", this);
  1.1329 +    Svc.Obs.remove("bookmarks-restore-success", this);
  1.1330 +    Svc.Obs.remove("bookmarks-restore-failed", this);
  1.1331 +  },
  1.1332 +
  1.1333 +  observe: function observe(subject, topic, data) {
  1.1334 +    Tracker.prototype.observe.call(this, subject, topic, data);
  1.1335 +
  1.1336 +    switch (topic) {
  1.1337 +      case "bookmarks-restore-begin":
  1.1338 +        this._log.debug("Ignoring changes from importing bookmarks.");
  1.1339 +        this.ignoreAll = true;
  1.1340 +        break;
  1.1341 +      case "bookmarks-restore-success":
  1.1342 +        this._log.debug("Tracking all items on successful import.");
  1.1343 +        this.ignoreAll = false;
  1.1344 +
  1.1345 +        this._log.debug("Restore succeeded: wiping server and other clients.");
  1.1346 +        this.engine.service.resetClient([this.name]);
  1.1347 +        this.engine.service.wipeServer([this.name]);
  1.1348 +        this.engine.service.clientsEngine.sendCommand("wipeEngine", [this.name]);
  1.1349 +        break;
  1.1350 +      case "bookmarks-restore-failed":
  1.1351 +        this._log.debug("Tracking all items on failed import.");
  1.1352 +        this.ignoreAll = false;
  1.1353 +        break;
  1.1354 +    }
  1.1355 +  },
  1.1356 +
  1.1357 +  QueryInterface: XPCOMUtils.generateQI([
  1.1358 +    Ci.nsINavBookmarkObserver,
  1.1359 +    Ci.nsINavBookmarkObserver_MOZILLA_1_9_1_ADDITIONS,
  1.1360 +    Ci.nsISupportsWeakReference
  1.1361 +  ]),
  1.1362 +
  1.1363 +  /**
  1.1364 +   * Add a bookmark GUID to be uploaded and bump up the sync score.
  1.1365 +   *
  1.1366 +   * @param itemGuid
  1.1367 +   *        GUID of the bookmark to upload.
  1.1368 +   */
  1.1369 +  _add: function BMT__add(itemId, guid) {
  1.1370 +    guid = kSpecialIds.specialGUIDForId(itemId) || guid;
  1.1371 +    if (this.addChangedID(guid))
  1.1372 +      this._upScore();
  1.1373 +  },
  1.1374 +
  1.1375 +  /* Every add/remove/change will trigger a sync for MULTI_DEVICE. */
  1.1376 +  _upScore: function BMT__upScore() {
  1.1377 +    this.score += SCORE_INCREMENT_XLARGE;
  1.1378 +  },
  1.1379 +
  1.1380 +  /**
  1.1381 +   * Determine if a change should be ignored.
  1.1382 +   *
  1.1383 +   * @param itemId
  1.1384 +   *        Item under consideration to ignore
  1.1385 +   * @param folder (optional)
  1.1386 +   *        Folder of the item being changed
  1.1387 +   */
  1.1388 +  _ignore: function BMT__ignore(itemId, folder, guid) {
  1.1389 +    // Ignore unconditionally if the engine tells us to.
  1.1390 +    if (this.ignoreAll)
  1.1391 +      return true;
  1.1392 +
  1.1393 +    // Get the folder id if we weren't given one.
  1.1394 +    if (folder == null) {
  1.1395 +      try {
  1.1396 +        folder = PlacesUtils.bookmarks.getFolderIdForItem(itemId);
  1.1397 +      } catch (ex) {
  1.1398 +        this._log.debug("getFolderIdForItem(" + itemId +
  1.1399 +                        ") threw; calling _ensureMobileQuery.");
  1.1400 +        // I'm guessing that gFIFI can throw, and perhaps that's why
  1.1401 +        // _ensureMobileQuery is here at all. Try not to call it.
  1.1402 +        this._ensureMobileQuery();
  1.1403 +        folder = PlacesUtils.bookmarks.getFolderIdForItem(itemId);
  1.1404 +      }
  1.1405 +    }
  1.1406 +
  1.1407 +    // Ignore changes to tags (folders under the tags folder).
  1.1408 +    let tags = kSpecialIds.tags;
  1.1409 +    if (folder == tags)
  1.1410 +      return true;
  1.1411 +
  1.1412 +    // Ignore tag items (the actual instance of a tag for a bookmark).
  1.1413 +    if (PlacesUtils.bookmarks.getFolderIdForItem(folder) == tags)
  1.1414 +      return true;
  1.1415 +
  1.1416 +    // Make sure to remove items that have the exclude annotation.
  1.1417 +    if (PlacesUtils.annotations.itemHasAnnotation(itemId, EXCLUDEBACKUP_ANNO)) {
  1.1418 +      this.removeChangedID(guid);
  1.1419 +      return true;
  1.1420 +    }
  1.1421 +
  1.1422 +    return false;
  1.1423 +  },
  1.1424 +
  1.1425 +  onItemAdded: function BMT_onItemAdded(itemId, folder, index,
  1.1426 +                                        itemType, uri, title, dateAdded,
  1.1427 +                                        guid, parentGuid) {
  1.1428 +    if (this._ignore(itemId, folder, guid))
  1.1429 +      return;
  1.1430 +
  1.1431 +    this._log.trace("onItemAdded: " + itemId);
  1.1432 +    this._add(itemId, guid);
  1.1433 +    this._add(folder, parentGuid);
  1.1434 +  },
  1.1435 +
  1.1436 +  onItemRemoved: function (itemId, parentId, index, type, uri,
  1.1437 +                           guid, parentGuid) {
  1.1438 +    if (this._ignore(itemId, parentId, guid)) {
  1.1439 +      return;
  1.1440 +    }
  1.1441 +
  1.1442 +    this._log.trace("onItemRemoved: " + itemId);
  1.1443 +    this._add(itemId, guid);
  1.1444 +    this._add(parentId, parentGuid);
  1.1445 +  },
  1.1446 +
  1.1447 +  _ensureMobileQuery: function _ensureMobileQuery() {
  1.1448 +    let find = function (val)
  1.1449 +      PlacesUtils.annotations.getItemsWithAnnotation(ORGANIZERQUERY_ANNO, {}).filter(
  1.1450 +        function (id) PlacesUtils.annotations.getItemAnnotation(id, ORGANIZERQUERY_ANNO) == val
  1.1451 +      );
  1.1452 +
  1.1453 +    // Don't continue if the Library isn't ready
  1.1454 +    let all = find(ALLBOOKMARKS_ANNO);
  1.1455 +    if (all.length == 0)
  1.1456 +      return;
  1.1457 +
  1.1458 +    // Disable handling of notifications while changing the mobile query
  1.1459 +    this.ignoreAll = true;
  1.1460 +
  1.1461 +    let mobile = find(MOBILE_ANNO);
  1.1462 +    let queryURI = Utils.makeURI("place:folder=" + kSpecialIds.mobile);
  1.1463 +    let title = Str.sync.get("mobile.label");
  1.1464 +
  1.1465 +    // Don't add OR remove the mobile bookmarks if there's nothing.
  1.1466 +    if (PlacesUtils.bookmarks.getIdForItemAt(kSpecialIds.mobile, 0) == -1) {
  1.1467 +      if (mobile.length != 0)
  1.1468 +        PlacesUtils.bookmarks.removeItem(mobile[0]);
  1.1469 +    }
  1.1470 +    // Add the mobile bookmarks query if it doesn't exist
  1.1471 +    else if (mobile.length == 0) {
  1.1472 +      let query = PlacesUtils.bookmarks.insertBookmark(all[0], queryURI, -1, title);
  1.1473 +      PlacesUtils.annotations.setItemAnnotation(query, ORGANIZERQUERY_ANNO, MOBILE_ANNO, 0,
  1.1474 +                                  PlacesUtils.annotations.EXPIRE_NEVER);
  1.1475 +      PlacesUtils.annotations.setItemAnnotation(query, EXCLUDEBACKUP_ANNO, 1, 0,
  1.1476 +                                  PlacesUtils.annotations.EXPIRE_NEVER);
  1.1477 +    }
  1.1478 +    // Make sure the existing title is correct
  1.1479 +    else if (PlacesUtils.bookmarks.getItemTitle(mobile[0]) != title) {
  1.1480 +      PlacesUtils.bookmarks.setItemTitle(mobile[0], title);
  1.1481 +    }
  1.1482 +
  1.1483 +    this.ignoreAll = false;
  1.1484 +  },
  1.1485 +
  1.1486 +  // This method is oddly structured, but the idea is to return as quickly as
  1.1487 +  // possible -- this handler gets called *every time* a bookmark changes, for
  1.1488 +  // *each change*.
  1.1489 +  onItemChanged: function BMT_onItemChanged(itemId, property, isAnno, value,
  1.1490 +                                            lastModified, itemType, parentId,
  1.1491 +                                            guid, parentGuid) {
  1.1492 +    // Quicker checks first.
  1.1493 +    if (this.ignoreAll)
  1.1494 +      return;
  1.1495 +
  1.1496 +    if (isAnno && (ANNOS_TO_TRACK.indexOf(property) == -1))
  1.1497 +      // Ignore annotations except for the ones that we sync.
  1.1498 +      return;
  1.1499 +
  1.1500 +    // Ignore favicon changes to avoid unnecessary churn.
  1.1501 +    if (property == "favicon")
  1.1502 +      return;
  1.1503 +
  1.1504 +    if (this._ignore(itemId, parentId, guid))
  1.1505 +      return;
  1.1506 +
  1.1507 +    this._log.trace("onItemChanged: " + itemId +
  1.1508 +                    (", " + property + (isAnno? " (anno)" : "")) +
  1.1509 +                    (value ? (" = \"" + value + "\"") : ""));
  1.1510 +    this._add(itemId, guid);
  1.1511 +  },
  1.1512 +
  1.1513 +  onItemMoved: function BMT_onItemMoved(itemId, oldParent, oldIndex,
  1.1514 +                                        newParent, newIndex, itemType,
  1.1515 +                                        guid, oldParentGuid, newParentGuid) {
  1.1516 +    if (this._ignore(itemId, newParent, guid))
  1.1517 +      return;
  1.1518 +
  1.1519 +    this._log.trace("onItemMoved: " + itemId);
  1.1520 +    this._add(oldParent, oldParentGuid);
  1.1521 +    if (oldParent != newParent) {
  1.1522 +      this._add(itemId, guid);
  1.1523 +      this._add(newParent, newParentGuid);
  1.1524 +    }
  1.1525 +
  1.1526 +    // Remove any position annotations now that the user moved the item
  1.1527 +    PlacesUtils.annotations.removeItemAnnotation(itemId, PARENT_ANNO);
  1.1528 +  },
  1.1529 +
  1.1530 +  onBeginUpdateBatch: function () {},
  1.1531 +  onEndUpdateBatch: function () {},
  1.1532 +  onItemVisited: function () {}
  1.1533 +};

mercurial