PageRenderTime 77ms CodeModel.GetById 7ms app.highlight 59ms RepoModel.GetById 1ms app.codeStats 0ms

/services/sync/modules/engines/bookmarks.js

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