engines/bookmarks.js

1176 lines, 734 LOC, 404 covered (55%)

5 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 Mozilla.
17
 * Portions created by the Initial Developer are Copyright (C) 2007
18
 * the Initial Developer. All Rights Reserved.
19
 *
20
 * Contributor(s):
21
 *  Dan Mills <thunder@mozilla.com>
22
 *  Jono DiCarlo <jdicarlo@mozilla.org>
23
 *  Anant Narayanan <anant@kix.in>
24
 *
25
 * Alternatively, the contents of this file may be used under the terms of
26
 * either the GNU General Public License Version 2 or later (the "GPL"), or
27
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
28
 * in which case the provisions of the GPL or the LGPL are applicable instead
29
 * of those above. If you wish to allow use of your version of this file only
30
 * under the terms of either the GPL or the LGPL, and not to allow others to
31
 * use your version of this file under the terms of the MPL, indicate your
32
 * decision by deleting the provisions above and replace them with the notice
33
 * and other provisions required by the GPL or the LGPL. If you do not delete
34
 * the provisions above, a recipient may use your version of this file under
35
 * the terms of any one of the MPL, the GPL or the LGPL.
36
 *
37
 * ***** END LICENSE BLOCK ***** */
38
50 39
const EXPORTED_SYMBOLS = ['BookmarksEngine', 'BookmarksSharingManager'];
40
20 41
const Cc = Components.classes;
20 42
const Ci = Components.interfaces;
20 43
const Cu = Components.utils;
44
15 45
const PARENT_ANNO = "weave/parent";
15 46
const PREDECESSOR_ANNO = "weave/predecessor";
15 47
const SERVICE_NOT_SUPPORTED = "Service not supported on this platform";
48
5 49
try {
20 50
  Cu.import("resource://gre/modules/PlacesUtils.jsm");
51
}
15 52
catch(ex) {
35 53
  Cu.import("resource://gre/modules/utils.js");
54
}
25 55
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
25 56
Cu.import("resource://weave/ext/Observers.js");
25 57
Cu.import("resource://weave/util.js");
25 58
Cu.import("resource://weave/engines.js");
25 59
Cu.import("resource://weave/stores.js");
25 60
Cu.import("resource://weave/trackers.js");
25 61
Cu.import("resource://weave/type_records/bookmark.js");
62
12 63
function archiveBookmarks() {
64
  // Some nightly builds of 3.7 don't have this function
2 65
  try {
14 66
    PlacesUtils.archiveBookmarksFile(null, true);
67
  }
2 68
  catch(ex) {}
69
}
70
71
// Lazily initialize the special top level folders
20 72
let kSpecialIds = {};
60 73
[["menu", "bookmarksMenuFolder"],
50 74
 ["places", "placesRoot"],
50 75
 ["tags", "tagsFolder"],
50 76
 ["toolbar", "toolbarFolder"],
55 77
 ["unfiled", "unfiledBookmarksFolder"],
290 78
].forEach(function([guid, placeName]) {
260 79
  Utils.lazy2(kSpecialIds, guid, function() Svc.Bookmark[placeName]);
80
});
37 81
Utils.lazy2(kSpecialIds, "mobile", function() {
82
  // Use the (one) mobile root if it already exists
4 83
  let anno = "mobile/bookmarksRoot";
16 84
  let root = Svc.Annos.getItemsWithAnnotation(anno, {});
8 85
  if (root.length != 0)
8 86
    return root[0];
87
88
  // Create the special mobile folder to store mobile bookmarks
89
  let mobile = Svc.Bookmark.createFolder(Svc.Bookmark.placesRoot, "mobile", -1);
90
  Utils.anno(mobile, anno, 1);
91
  return mobile;
92
});
93
94
// Create some helper functions to convert GUID/ids
206 95
function idForGUID(guid) {
588 96
  if (guid in kSpecialIds)
92 97
    return kSpecialIds[guid];
1038 98
  return Svc.Bookmark.getItemIdForGUID(guid);
99
}
291 100
function GUIDForId(placeId) {
20067 101
  for (let [guid, id] in Iterator(kSpecialIds))
5013 102
    if (placeId == id)
4479 103
      return guid;
1608 104
  return Svc.Bookmark.getItemGUID(placeId);
105
}
106
15 107
function BookmarksEngine() {
30 108
  SyncEngine.call(this, "Bookmarks");
25 109
  this._handleImport();
110
}
10 111
BookmarksEngine.prototype = {
15 112
  __proto__: SyncEngine.prototype,
10 113
  _recordObj: PlacesItem,
10 114
  _storeObj: BookmarksStore,
10 115
  _trackerObj: BookmarksTracker,
116
15 117
  _handleImport: function _handleImport() {
20 118
    Observers.add("bookmarks-restore-begin", function() {
119
      this._log.debug("Ignoring changes from importing bookmarks");
120
      this._tracker.ignoreAll = true;
15 121
    }, this);
122
20 123
    Observers.add("bookmarks-restore-success", function() {
124
      this._log.debug("Tracking all items on successful import");
125
      this._tracker.ignoreAll = false;
126
127
      // Mark all the items as changed so they get uploaded
128
      for (let id in this._store.getAllIDs())
129
        this._tracker.addChangedID(id);
15 130
    }, this);
131
20 132
    Observers.add("bookmarks-restore-failed", function() {
133
      this._tracker.ignoreAll = false;
20 134
    }, this);
135
  },
136
30 137
  _sync: Utils.batchSync("Bookmark", SyncEngine),
138
10 139
  _syncStartup: function _syncStart() {
140
    SyncEngine.prototype._syncStartup.call(this);
141
142
    // For first-syncs, make a backup for the user to restore
143
    if (this.lastSync == 0)
144
      archiveBookmarks();
145
146
    // Lazily create a mapping of folder titles and separator positions to GUID
147
    this.__defineGetter__("_lazyMap", function() {
148
      delete this._lazyMap;
149
150
      let lazyMap = {};
151
      for (let guid in this._store.getAllIDs()) {
152
        // Figure out what key to store the mapping
153
        let key;
154
        let id = idForGUID(guid);
155
        switch (Svc.Bookmark.getItemType(id)) {
156
          case Svc.Bookmark.TYPE_BOOKMARK:
157
            key = "b" + Svc.Bookmark.getBookmarkURI(id).spec + ":" +
158
              Svc.Bookmark.getItemTitle(id);
159
            break;
160
          case Svc.Bookmark.TYPE_FOLDER:
161
            key = "f" + Svc.Bookmark.getItemTitle(id);
162
            break;
163
          case Svc.Bookmark.TYPE_SEPARATOR:
164
            key = "s" + Svc.Bookmark.getItemIndex(id);
165
            break;
166
          default:
167
            continue;
168
        }
169
170
        // The mapping is on a per parent-folder-name basis
171
        let parent = Svc.Bookmark.getFolderIdForItem(id);
172
        let parentName = Svc.Bookmark.getItemTitle(parent);
173
        if (lazyMap[parentName] == null)
174
          lazyMap[parentName] = {};
175
176
        // If the entry already exists, remember that there are explicit dupes
177
        let entry = new String(guid);
178
        entry.hasDupe = lazyMap[parentName][key] != null;
179
180
        // Remember this item's guid for its parent-name/key pair
181
        lazyMap[parentName][key] = entry;
182
        this._log.trace("Mapped: " + [parentName, key, entry, entry.hasDupe]);
183
      }
184
185
      // Expose a helper function to get a dupe guid for an item
186
      return this._lazyMap = function(item) {
187
        // Figure out if we have something to key with
188
        let key;
189
        switch (item.type) {
190
          case "bookmark":
191
          case "query":
192
          case "microsummary":
193
            key = "b" + item.bmkUri + ":" + item.title;
194
            break;
195
          case "folder":
196
          case "livemark":
197
            key = "f" + item.title;
198
            break;
199
          case "separator":
200
            key = "s" + item.pos;
201
            break;
202
          default:
203
            return;
204
        }
205
206
        // Give the guid if we have the matching pair
207
        this._log.trace("Finding mapping: " + item.parentName + ", " + key);
208
        let parent = lazyMap[item.parentName];
209
        let dupe = parent && parent[key];
210
        this._log.trace("Mapped dupe: " + dupe);
211
        return dupe;
212
      };
213
    });
214
  },
215
10 216
  _syncFinish: function _syncFinish() {
217
    SyncEngine.prototype._syncFinish.call(this);
218
    delete this._lazyMap;
219
    this._tracker._ensureMobileQuery();
220
  },
221
10 222
  _createRecord: function _createRecord(id) {
223
    // Create the record like normal but mark it as having dupes if necessary
224
    let record = SyncEngine.prototype._createRecord.call(this, id);
225
    let entry = this._lazyMap(record);
226
    if (entry != null && entry.hasDupe)
227
      record.hasDupe = true;
228
    return record;
229
  },
230
10 231
  _findDupe: function _findDupe(item) {
232
    // Don't bother finding a dupe if the incoming item has duplicates
233
    if (item.hasDupe)
234
      return;
235
    return this._lazyMap(item);
236
  },
237
25 238
  _handleDupe: function _handleDupe(item, dupeId) {
239
    // The local dupe has the lower id, so make it the winning id
240
    if (dupeId < item.id)
241
      [item.id, dupeId] = [dupeId, item.id];
242
243
    // Trigger id change from dupe to winning and update the server
244
    this._store.changeItemID(dupeId, item.id);
245
    this._deleteId(dupeId);
246
    this._tracker.addChangedID(item.id, 0);
247
  }
248
};
249
12 250
function BookmarksStore(name) {
14 251
  Store.call(this, name);
252
}
10 253
BookmarksStore.prototype = {
15 254
  __proto__: Store.prototype,
255
10 256
  __bms: null,
169 257
  get _bms() {
477 258
    if (!this.__bms)
8 259
      this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
8 260
                   getService(Ci.nsINavBookmarksService);
318 261
    return this.__bms;
262
  },
263
10 264
  __hsvc: null,
10 265
  get _hsvc() {
266
    if (!this.__hsvc)
267
      this.__hsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
268
                    getService(Ci.nsINavHistoryService);
269
    return this.__hsvc;
270
  },
271
10 272
  __ls: null,
10 273
  get _ls() {
274
    if (!this.__ls)
275
      this.__ls = Cc["@mozilla.org/browser/livemark-service;2"].
276
        getService(Ci.nsILivemarkService);
277
    return this.__ls;
278
  },
279
11 280
  get _ms() {
2 281
    let ms;
1 282
    try {
3 283
      ms = Cc["@mozilla.org/microsummary/service;1"].
5 284
        getService(Ci.nsIMicrosummaryService);
285
    } catch (e) {
286
      ms = null;
287
      this._log.warn("Could not load microsummary service");
288
      this._log.debug(e);
289
    }
27 290
    this.__defineGetter__("_ms", function() ms);
2 291
    return ms;
292
  },
293
10 294
  __ts: null,
146 295
  get _ts() {
408 296
    if (!this.__ts)
8 297
      this.__ts = Cc["@mozilla.org/browser/tagging-service;1"].
8 298
                  getService(Ci.nsITaggingService);
272 299
    return this.__ts;
300
  },
301
302
52 303
  itemExists: function BStore_itemExists(id) {
252 304
    return idForGUID(id) > 0;
305
  },
306
307
  // Hash of old GUIDs to the new renamed GUIDs
15 308
  aliases: {},
309
52 310
  applyIncoming: function BStore_applyIncoming(record) {
311
    // Ignore (accidental?) root changes
126 312
    if (record.id in kSpecialIds) {
313
      this._log.debug("Skipping change to root node: " + record.id);
314
      return;
315
    }
316
317
    // Convert GUID fields to the aliased GUID if necessary
378 318
    ["id", "parentid", "predecessorid"].forEach(function(field) {
756 319
      let alias = this.aliases[record[field]];
378 320
      if (alias != null)
126 321
        record[field] = alias;
126 322
    }, this);
323
324
    // Preprocess the record before doing the normal apply
84 325
    switch (record.type) {
326
      case "query": {
327
        // Convert the query uri if necessary
328
        if (record.bmkUri == null || record.folderName == null)
329
          break;
330
331
        // Tag something so that the tag exists
332
        let tag = record.folderName;
333
        let dummyURI = Utils.makeURI("about:weave#BStore_preprocess");
334
        this._ts.tagURI(dummyURI, [tag]);
335
336
        // Look for the id of the tag (that might have just been added)
337
        let tags = this._getNode(this._bms.tagsFolder);
338
        if (!(tags instanceof Ci.nsINavHistoryQueryResultNode))
339
          break;
340
341
        tags.containerOpen = true;
342
        for (let i = 0; i < tags.childCount; i++) {
343
          let child = tags.getChild(i);
344
          // Found the tag, so fix up the query to use the right id
345
          if (child.title == tag) {
346
            this._log.debug("query folder: " + tag + " = " + child.itemId);
347
            record.bmkUri = record.bmkUri.replace(/([:&]folder=)\d+/, "$1" +
348
              child.itemId);
349
            break;
350
          }
351
        }
352
        break;
353
      }
354
    }
355
356
    // Figure out the local id of the parent GUID if available
84 357
    let parentGUID = record.parentid;
126 358
    record._orphan = false;
168 359
    if (parentGUID != null) {
168 360
      let parentId = idForGUID(parentGUID);
361
362
      // Default to unfiled if we don't have the parent yet
126 363
      if (parentId <= 0) {
12 364
        this._log.trace("Reparenting to unfiled until parent is synced");
6 365
        record._orphan = true;
6 366
        parentId = kSpecialIds.unfiled;
367
      }
368
369
      // Save the parent id for modifying the bookmark later
168 370
      record._parent = parentId;
371
    }
372
373
    // Default to append unless we're not an orphan with the predecessor
84 374
    let predGUID = record.predecessorid;
210 375
    record._insertPos = Svc.Bookmark.DEFAULT_INDEX;
126 376
    if (!record._orphan) {
377
      // No predecessor means it's the first item
120 378
      if (predGUID == null)
64 379
        record._insertPos = 0;
24 380
      else {
381
        // The insert position is one after the predecessor of the same parent
96 382
        let predId = idForGUID(predGUID);
235 383
        if (predId != -1 && this._getParentGUIDForId(predId) == parentGUID) {
162 384
          record._insertPos = Svc.Bookmark.getItemIndex(predId) + 1;
72 385
          record._predId = predId;
386
        }
387
        else
60 388
          this._log.trace("Appending to end until predecessor is synced");
389
      }
390
    }
391
392
    // Do the normal processing of incoming records
336 393
    Store.prototype.applyIncoming.apply(this, arguments);
394
395
    // Do some post-processing if we have an item
168 396
    let itemId = idForGUID(record.id);
126 397
    if (itemId > 0) {
398
      // Move any children that are looking for this folder as a parent
126 399
      if (record.type == "folder")
45 400
        this._reparentOrphans(itemId);
401
402
      // Create an annotation to remember that it needs a parent
403
      // XXX Work around Bug 510628 by prepending parenT
84 404
      if (record._orphan)
20 405
        Utils.anno(itemId, PARENT_ANNO, "T" + parentGUID);
406
      // It's now in the right folder, so move annotated items behind this
407
      else
200 408
        this._attachFollowers(itemId);
409
410
      // Create an annotation if we have a predecessor but no position
411
      // XXX Work around Bug 510628 by prepending predecessoR
310 412
      if (predGUID != null && record._insertPos == Svc.Bookmark.DEFAULT_INDEX)
63 413
        Utils.anno(itemId, PREDECESSOR_ANNO, "R" + predGUID);
42 414
    }
415
  },
416
417
  /**
418
   * Find all ids of items that have a given value for an annotation
419
   */
61 420
  _findAnnoItems: function BStore__findAnnoItems(anno, val) {
421
    // XXX Work around Bug 510628 by prepending parenT
153 422
    if (anno == PARENT_ANNO)
45 423
      val = "T" + val;
424
    // XXX Work around Bug 510628 by prepending predecessoR
126 425
    else if (anno == PREDECESSOR_ANNO)
168 426
      val = "R" + val;
427
573 428
    return Svc.Annos.getItemsWithAnnotation(anno, {}).filter(function(id)
96 429
      Utils.anno(id, anno) == val);
430
  },
431
432
  /**
433
   * For the provided parent item, attach its children to it
434
   */
19 435
  _reparentOrphans: function _reparentOrphans(parentId) {
436
    // Find orphans and reunite with this folder parent
36 437
    let parentGUID = GUIDForId(parentId);
54 438
    let orphans = this._findAnnoItems(PARENT_ANNO, parentGUID);
439
90 440
    this._log.debug("Reparenting orphans " + orphans + " to " + parentId);
47 441
    orphans.forEach(function(orphan) {
442
      // Append the orphan under the parent unless it's supposed to be first
8 443
      let insertPos = Svc.Bookmark.DEFAULT_INDEX;
16 444
      if (!Svc.Annos.itemHasAnnotation(orphan, PREDECESSOR_ANNO))
2 445
        insertPos = 0;
446
447
      // Move the orphan to the parent and drop the missing parent annotation
16 448
      Svc.Bookmark.moveItem(orphan, parentId, insertPos);
16 449
      Svc.Annos.removeItemAnnotation(orphan, PARENT_ANNO);
450
    });
451
452
    // Fix up the ordering of the now-parented items
63 453
    orphans.forEach(this._attachFollowers, this);
454
  },
455
456
  /**
457
   * Move an item and all of its followers to a new position until reaching an
458
   * item that shouldn't be moved
459
   */
29 460
  _moveItemChain: function BStore__moveItemChain(itemId, insertPos, stopId) {
114 461
    let parentId = Svc.Bookmark.getFolderIdForItem(itemId);
462
463
    // Keep processing the item chain until it loops to the stop item
105 464
    do {
465
      // Figure out what's next in the chain
258 466
      let itemPos = Svc.Bookmark.getItemIndex(itemId);
387 467
      let nextId = Svc.Bookmark.getIdForItemAt(parentId, itemPos + 1);
468
344 469
      Svc.Bookmark.moveItem(itemId, parentId, insertPos);
430 470
      this._log.trace("Moved " + itemId + " to " + insertPos);
471
472
      // Prepare for the next item in the chain
344 473
      insertPos = Svc.Bookmark.getItemIndex(itemId) + 1;
86 474
      itemId = nextId;
475
476
      // Stop if we ran off the end or the item is looking for its pred.
395 477
      if (itemId == -1 || Svc.Annos.itemHasAnnotation(itemId, PREDECESSOR_ANNO))
56 478
        break;
109 479
    } while (itemId != stopId);
480
  },
481
482
  /**
483
   * For the provided predecessor item, attach its followers to it
484
   */
52 485
  _attachFollowers: function BStore__attachFollowers(predId) {
168 486
    let predGUID = GUIDForId(predId);
252 487
    let followers = this._findAnnoItems(PREDECESSOR_ANNO, predGUID);
168 488
    if (followers.length > 1)
489
      this._log.warn(predId + " has more than one followers: " + followers);
490
491
    // Start at the first follower and move the chain of followers
252 492
    let parent = Svc.Bookmark.getFolderIdForItem(predId);
132 493
    followers.forEach(function(follow) {
60 494
      this._log.trace("Repositioning " + follow + " behind " + predId);
42 495
      if (Svc.Bookmark.getFolderIdForItem(follow) != parent) {
496
        this._log.warn("Follower doesn't have the same parent: " + parent);
497
        return;
498
      }
499
500
      // Move the chain of followers to after the predecessor
48 501
      let insertPos = Svc.Bookmark.getItemIndex(predId) + 1;
42 502
      this._moveItemChain(follow, insertPos, predId);
503
504
      // Remove the annotation now that we're putting it in the right spot
48 505
      Svc.Annos.removeItemAnnotation(follow, PREDECESSOR_ANNO);
168 506
    }, this);
507
  },
508
18 509
  create: function BStore_create(record) {
16 510
    let newId;
24 511
    switch (record.type) {
512
    case "bookmark":
513
    case "query":
6 514
    case "microsummary": {
30 515
      let uri = Utils.makeURI(record.bmkUri);
36 516
      newId = this._bms.insertBookmark(record._parent, uri, record._insertPos,
18 517
        record.title);
48 518
      this._log.debug(["created bookmark", newId, "under", record._parent, "at",
60 519
        record._insertPos, "as", record.title, record.bmkUri].join(" "));
520
36 521
      this._tagURI(uri, record.tags);
42 522
      this._bms.setKeywordForBookmark(newId, record.keyword);
12 523
      if (record.description)
524
        Utils.anno(newId, "bookmarkProperties/description", record.description);
525
12 526
      if (record.loadInSidebar)
527
        Utils.anno(newId, "bookmarkProperties/loadInSidebar", true);
528
18 529
      if (record.type == "microsummary") {
530
        this._log.debug("   \-> is a microsummary");
531
        Utils.anno(newId, "bookmarks/staticTitle", record.staticTitle || "");
532
        let genURI = Utils.makeURI(record.generatorUri);
533
        if (this._ms) {
534
          try {
535
            let micsum = this._ms.createMicrosummary(uri, genURI);
536
            this._ms.setMicrosummary(newId, micsum);
537
          }
538
          catch(ex) { /* ignore "missing local generator" exceptions */ }
539
        }
540
        else
6 541
          this._log.warn("Can't create microsummary -- not supported.");
542
      }
6 543
    } break;
544
    case "folder":
10 545
      newId = this._bms.createFolder(record._parent, record.title,
6 546
        record._insertPos);
16 547
      this._log.debug(["created folder", newId, "under", record._parent, "at",
18 548
        record._insertPos, "as", record.title].join(" "));
549
4 550
      if (record.description)
551
        Utils.anno(newId, "bookmarkProperties/description", record.description);
2 552
      break;
553
    case "livemark":
554
      let siteURI = null;
555
      if (record.siteUri != null)
556
        siteURI = Utils.makeURI(record.siteUri);
557
558
      newId = this._ls.createLivemark(record._parent, record.title, siteURI,
559
        Utils.makeURI(record.feedUri), record._insertPos);
560
      this._log.debug(["created livemark", newId, "under", record._parent, "at",
561
        record._insertPos, "as", record.title, record.siteUri, record.feedUri].
562
        join(" "));
563
      break;
564
    case "separator":
565
      newId = this._bms.insertSeparator(record._parent, record._insertPos);
566
      this._log.debug(["created separator", newId, "under", record._parent,
567
        "at", record._insertPos].join(" "));
568
      break;
569
    case "item":
570
      this._log.debug(" -> got a generic places item.. do nothing?");
571
      return;
572
    default:
573
      this._log.error("_create: Unknown item type: " + record.type);
8 574
      return;
575
    }
576
80 577
    this._log.trace("Setting GUID of new item " + newId + " to " + record.id);
56 578
    this._setGUID(newId, record.id);
579
  },
580
10 581
  remove: function BStore_remove(record) {
582
    let itemId = idForGUID(record.id);
583
    if (itemId <= 0) {
584
      this._log.debug("Item " + record.id + " already removed");
585
      return;
586
    }
587
    var type = this._bms.getItemType(itemId);
588
589
    switch (type) {
590
    case this._bms.TYPE_BOOKMARK:
591
      this._log.debug("  -> removing bookmark " + record.id);
592
      this._ts.untagURI(this._bms.getBookmarkURI(itemId), null);
593
      this._bms.removeItem(itemId);
594
      break;
595
    case this._bms.TYPE_FOLDER:
596
      this._log.debug("  -> removing folder " + record.id);
597
      Svc.Bookmark.removeItem(itemId);
598
      break;
599
    case this._bms.TYPE_SEPARATOR:
600
      this._log.debug("  -> removing separator " + record.id);
601
      this._bms.removeItem(itemId);
602
      break;
603
    default:
604
      this._log.error("remove: Unknown item type: " + type);
605
      break;
606
    }
607
  },
608
44 609
  update: function BStore_update(record) {
136 610
    let itemId = idForGUID(record.id);
611
102 612
    if (itemId <= 0) {
613
      this._log.debug("Skipping update for unknown item: " + record.id);
614
      return;
615
    }
616
374 617
    this._log.trace("Updating " + record.id + " (" + itemId + ")");
618
619
    // Move the bookmark to a new parent if necessary
238 620
    if (Svc.Bookmark.getFolderIdForItem(itemId) != record._parent) {
66 621
      this._log.trace("Moving item to a new parent");
99 622
      Svc.Bookmark.moveItem(itemId, record._parent, record._insertPos);
623
    }
624
    // Move the chain of bookmarks to a new position
184 625
    else if (Svc.Bookmark.getItemIndex(itemId) != record._insertPos &&
49 626
             !record._orphan) {
78 627
      this._log.trace("Moving item and followers to a new position");
628
629
      // Stop moving at the predecessor unless we don't have one
113 630
      this._moveItemChain(itemId, record._insertPos, record._predId || itemId);
631
    }
632
2668 633
    for (let [key, val] in Iterator(record.cleartext)) {
448 634
      switch (key) {
635
      case "title":
238 636
        this._bms.setItemTitle(itemId, val);
34 637
        break;
638
      case "bmkUri":
270 639
        this._bms.changeBookmarkURI(itemId, Utils.makeURI(val));
27 640
        break;
641
      case "tags":
270 642
        this._tagURI(this._bms.getBookmarkURI(itemId), val);
27 643
        break;
644
      case "keyword":
645
        this._bms.setKeywordForBookmark(itemId, val);
646
        break;
647
      case "description":
648
        Utils.anno(itemId, "bookmarkProperties/description", val);
649
        break;
650
      case "loadInSidebar":
651
        if (val)
652
          Utils.anno(itemId, "bookmarkProperties/loadInSidebar", true);
653
        else
654
          Svc.Annos.removeItemAnnotation(itemId, "bookmarkProperties/loadInSidebar");
655
        break;
656
      case "generatorUri": {
657
        try {
658
          let micsumURI = this._bms.getBookmarkURI(itemId);
659
          let genURI = Utils.makeURI(val);
660
          if (this._ms == SERVICE_NOT_SUPPORTED)
661
            this._log.warn("Can't create microsummary -- not supported.");
662
          else {
663
            let micsum = this._ms.createMicrosummary(micsumURI, genURI);
664
            this._ms.setMicrosummary(itemId, micsum);
665
          }
666
        } catch (e) {
667
          this._log.debug("Could not set microsummary generator URI: " + e);
668
        }
669
      } break;
670
      case "siteUri":
671
        this._ls.setSiteURI(itemId, Utils.makeURI(val));
672
        break;
673
      case "feedUri":
674
        this._ls.setFeedURI(itemId, Utils.makeURI(val));
584 675
        break;
676
      }
34 677
    }
678
  },
679
10 680
  changeItemID: function BStore_changeItemID(oldID, newID) {
681
    // Remember the GUID change for incoming records
682
    this.aliases[oldID] = newID;
683
684
    // Update any existing annotation references
685
    this._findAnnoItems(PARENT_ANNO, oldID).forEach(function(itemId) {
686
      Utils.anno(itemId, PARENT_ANNO, "T" + newID);
687
    }, this);
688
    this._findAnnoItems(PREDECESSOR_ANNO, oldID).forEach(function(itemId) {
689
      Utils.anno(itemId, PREDECESSOR_ANNO, "R" + newID);
690
    }, this);
691
692
    // Make sure there's an item to change GUIDs
693
    let itemId = idForGUID(oldID);
694
    if (itemId <= 0)
695
      return;
696
697
    this._log.debug("Changing GUID " + oldID + " to " + newID);
698
    this._setGUID(itemId, newID);
699
  },
700
18 701
  _setGUID: function BStore__setGUID(itemId, guid) {
32 702
    let collision = idForGUID(guid);
24 703
    if (collision != -1) {
704
      this._log.warn("Freeing up GUID " + guid  + " used by " + collision);
705
      Svc.Annos.removeItemAnnotation(collision, "placesInternal/GUID");
706
    }
64 707
    Svc.Bookmark.setItemGUID(itemId, guid);
708
  },
709
10 710
  _getNode: function BStore__getNode(folder) {
711
    let query = this._hsvc.getNewQuery();
712
    query.setFolders([folder], 1);
713
    return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root;
714
  },
715
14 716
  _getTags: function BStore__getTags(uri) {
4 717
    try {
16 718
      if (typeof(uri) == "string")
24 719
        uri = Utils.makeURI(uri);
720
    } catch(e) {
721
      this._log.warn("Could not parse URI \"" + uri + "\": " + e);
722
    }
32 723
    return this._ts.getTagsForURI(uri, {});
724
  },
725
14 726
  _getDescription: function BStore__getDescription(id) {
4 727
    try {
20 728
      return Utils.anno(id, "bookmarkProperties/description");
12 729
    } catch (e) {
16 730
      return undefined;
731
    }
732
  },
733
14 734
  _isLoadInSidebar: function BStore__isLoadInSidebar(id) {
28 735
    return Svc.Annos.itemHasAnnotation(id, "bookmarkProperties/loadInSidebar");
736
  },
737
10 738
  _getStaticTitle: function BStore__getStaticTitle(id) {
739
    try {
740
      return Utils.anno(id, "bookmarks/staticTitle");
741
    } catch (e) {
742
      return "";
743
    }
744
  },
745
746
  // Create a record starting from the weave id (places guid)
14 747
  createRecord: function createRecord(guid) {
16 748
    let placeId = idForGUID(guid);
8 749
    let record;
12 750
    if (placeId <= 0) { // deleted item
751
      record = new PlacesItem();
752
      record.deleted = true;
753
      return record;
754
    }
755
24 756
    let parent = Svc.Bookmark.getFolderIdForItem(placeId);
28 757
    switch (this._bms.getItemType(placeId)) {
16 758
    case this._bms.TYPE_BOOKMARK:
28 759
      let bmkUri = this._bms.getBookmarkURI(placeId).spec;
32 760
      if (this._ms && this._ms.hasMicrosummary(placeId)) {
761
        record = new BookmarkMicsum();
762
        let micsum = this._ms.getMicrosummary(placeId);
763
        record.generatorUri = micsum.generator.uri.spec; // breaks local generators
764
        record.staticTitle = this._getStaticTitle(placeId);
765
      }
766
      else {
24 767
        if (bmkUri.search(/^place:/) == 0) {
768
          record = new BookmarkQuery();
769
770
          // Get the actual tag name instead of the local itemId
771
          let folder = bmkUri.match(/[:&]folder=(\d+)/);
772
          try {
773
            // There might not be the tag yet when creating on a new client
774
            if (folder != null) {
775
              folder = folder[1];
776
              record.folderName = this._bms.getItemTitle(folder);
777
              this._log.debug("query id: " + folder + " = " + record.folderName);
778
            }
779
          }
780
          catch(ex) {}
781
        }
782
        else
12 783
          record = new Bookmark();
28 784
        record.title = this._bms.getItemTitle(placeId);
785
      }
786
28 787
      record.parentName = Svc.Bookmark.getItemTitle(parent);
12 788
      record.bmkUri = bmkUri;
24 789
      record.tags = this._getTags(record.bmkUri);
28 790
      record.keyword = this._bms.getKeywordForBookmark(placeId);
24 791
      record.description = this._getDescription(placeId);
24 792
      record.loadInSidebar = this._isLoadInSidebar(placeId);
4 793
      break;
794
795
    case this._bms.TYPE_FOLDER:
796
      if (this._ls.isLivemark(placeId)) {
797
        record = new Livemark();
798
799
        let siteURI = this._ls.getSiteURI(placeId);
800
        if (siteURI != null)
801
          record.siteUri = siteURI.spec;
802
        record.feedUri = this._ls.getFeedURI(placeId).spec;
803
804
      } else {
805
        record = new BookmarkFolder();
806
      }
807
808
      record.parentName = Svc.Bookmark.getItemTitle(parent);
809
      record.title = this._bms.getItemTitle(placeId);
810
      record.description = this._getDescription(placeId);
811
      break;
812
813
    case this._bms.TYPE_SEPARATOR:
814
      record = new BookmarkSeparator();
815
      // Create a positioning identifier for the separator
816
      record.parentName = Svc.Bookmark.getItemTitle(parent);
817
      record.pos = Svc.Bookmark.getItemIndex(placeId);
818
      break;
819
820
    case this._bms.TYPE_DYNAMIC_CONTAINER:
821
      record = new PlacesItem();
822
      this._log.warn("Don't know how to serialize dynamic containers yet");
823
      break;
824
825
    default:
826
      record = new PlacesItem();
827
      this._log.warn("Unknown item type, cannot serialize: " +
4 828
                     this._bms.getItemType(placeId));
829
    }
830
24 831
    record.parentid = this._getParentGUIDForId(placeId);
24 832
    record.predecessorid = this._getPredecessorGUIDForId(placeId);
24 833
    record.sortindex = this._calculateIndex(record);
834
8 835
    return record;
836
  },
837
11 838
  get _frecencyStm() {
6 839
    this._log.trace("Creating SQL statement: _frecencyStm");
4 840
    let stm = Svc.History.DBConnection.createStatement(
3 841
      "SELECT frecency " +
842
      "FROM moz_places " +
843
      "WHERE url = :url");
51 844
    this.__defineGetter__("_frecencyStm", function() stm);
2 845
    return stm;
846
  },
847
14 848
  _calculateIndex: function _calculateIndex(record) {
849
    // For anything directly under the toolbar, give it a boost of more than an
850
    // unvisited bookmark
8 851
    let index = 0;
12 852
    if (record.parentid == "toolbar")
8 853
      index += 150;
854
855
    // Add in the bookmark's frecency if we have something
12 856
    if (record.bmkUri != null) {
4 857
      try {
20 858
        this._frecencyStm.params.url = record.bmkUri;
20 859
        if (this._frecencyStm.step())
36 860
          index += this._frecencyStm.row.frecency;
861
      }
4 862
      finally {
24 863
        this._frecencyStm.reset();
864
      }
865
    }
866
8 867
    return index;
868
  },
869
37 870
  _getParentGUIDForId: function BStore__getParentGUIDForId(itemId) {
871
    // Give the parent annotation if it exists
27 872
    try {
873
      // XXX Work around Bug 510628 by removing prepended parenT
135 874
      return Utils.anno(itemId, PARENT_ANNO).slice(1);
875
    }
135 876
    catch(ex) {}
877
162 878
    let parentid = this._bms.getFolderIdForItem(itemId);
81 879
    if (parentid == -1) {
880
      this._log.debug("Found orphan bookmark, reparenting to unfiled");
881
      parentid = this._bms.unfiledBookmarksFolder;
882
      this._bms.moveItem(itemId, parentid, -1);
883
    }
108 884
    return GUIDForId(parentid);
885
  },
886
14 887
  _getPredecessorGUIDForId: function BStore__getPredecessorGUIDForId(itemId) {
888
    // Give the predecessor annotation if it exists
4 889
    try {
890
      // XXX Work around Bug 510628 by removing prepended predecessoR
20 891
      return Utils.anno(itemId, PREDECESSOR_ANNO).slice(1);
892
    }
20 893
    catch(ex) {}
894
895
    // Figure out the predecessor, unless it's the first item
24 896
    let itemPos = Svc.Bookmark.getItemIndex(itemId);
12 897
    if (itemPos == 0)
4 898
      return;
899
900
    // For items directly under unfiled/unsorted, give no predecessor
12 901
    let parentId = Svc.Bookmark.getFolderIdForItem(itemId);
10 902
    if (parentId == Svc.Bookmark.unfiledBookmarksFolder)
2 903
      return;
904
9 905
    let predecessorId = Svc.Bookmark.getIdForItemAt(parentId, itemPos - 1);
3 906
    if (predecessorId == -1) {
907
      this._log.debug("No predecessor directly before " + itemId + " under " +
908
        parentId + " at " + itemPos);
909
910
      // Find the predecessor before the item
911
      do {
912
        // No more items to check, it must be the first one
913
        if (--itemPos < 0)
914
          break;
915
        predecessorId = Svc.Bookmark.getIdForItemAt(parentId, itemPos);
916
      } while (predecessorId == -1);
917
918
      // Fix up the item to be at the right position for next time
919
      itemPos++;
920
      this._log.debug("Fixing " + itemId + " to be at position " + itemPos);
921
      Svc.Bookmark.moveItem(itemId, parentId, itemPos);
922
923
      // There must be no predecessor for this item!
924
      if (itemPos == 0)
925
        return;
926
    }
927
4 928
    return GUIDForId(predecessorId);
929
  },
930
10 931
  _getChildren: function BStore_getChildren(guid, items) {
932
    let node = guid; // the recursion case
933
    if (typeof(node) == "string") // callers will give us the guid as the first arg
934
      node = this._getNode(idForGUID(guid));
935
936
    if (node.type == node.RESULT_TYPE_FOLDER &&
937
        !this._ls.isLivemark(node.itemId)) {
938
      node.QueryInterface(Ci.nsINavHistoryQueryResultNode);
939
      node.containerOpen = true;
940
941
      // Remember all the children GUIDs and recursively get more
942
      for (var i = 0; i < node.childCount; i++) {
943
        let child = node.getChild(i);
944
        items[GUIDForId(child.itemId)] = true;
945
        this._getChildren(child, items);
946
      }
947
    }
948
949
    return items;
950
  },
951
43 952
  _tagURI: function BStore_tagURI(bmkURI, tags) {
953
    // Filter out any null/undefined/empty tags
165 954
    tags = tags.filter(function(t) t);
955
956
    // Temporarily tag a dummy uri to preserve tag ids when untagging
165 957
    let dummyURI = Utils.makeURI("about:weave#BStore_tagURI");
231 958
    this._ts.tagURI(dummyURI, tags);
231 959
    this._ts.untagURI(bmkURI, null);
231 960
    this._ts.tagURI(bmkURI, tags);
264 961
    this._ts.untagURI(dummyURI, null);
962
  },
963
10 964
  getAllIDs: function BStore_getAllIDs() {
965
    let items = {};
966
    for (let [guid, id] in Iterator(kSpecialIds))
967
      if (guid != "places" && guid != "tags")
968
        this._getChildren(guid, items);
969
    return items;
970
  },
971
27 972
  wipe: function BStore_wipe() {
973
    // Save a backup before clearing out all bookmarks
6 974
    archiveBookmarks();
975
144 976
    for (let [guid, id] in Iterator(kSpecialIds))
36 977
      if (guid != "places")
94 978
        this._bms.removeFolderChildren(id);
979
  }
980
};
981
15 982
function BookmarksTracker(name) {
30 983
  Tracker.call(this, name);
984
985
  // Ignore changes to the special roots
80 986
  for (let guid in kSpecialIds)
230 987
    this.ignoreID(guid);
988
40 989
  Svc.Bookmark.addObserver(this, false);
990
}
10 991
BookmarksTracker.prototype = {
15 992
  __proto__: Tracker.prototype,
993
12 994
  get _bms() {
6 995
    let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
8 996
      getService(Ci.nsINavBookmarksService);
1572 997
    this.__defineGetter__("_bms", function() bms);
4 998
    return bms;
999
  },
1000
12 1001
  get _ls() {
6 1002
    let ls = Cc["@mozilla.org/browser/livemark-service;2"].
8 1003
      getService(Ci.nsILivemarkService);
807 1004
    this.__defineGetter__("_ls", function() ls);
4 1005
    return ls;
1006
  },
1007
20 1008
  QueryInterface: XPCOMUtils.generateQI([
20 1009
    Ci.nsINavBookmarkObserver,
30 1010
    Ci.nsINavBookmarkObserver_MOZILLA_1_9_1_ADDITIONS
1011
  ]),
1012
1013
  /**
1014
   * Add a bookmark (places) id to be uploaded and bump up the sync score
1015
   *
1016
   * @param itemId
1017
   *        Places internal id of the bookmark to upload
1018
   */
212 1019
  _addId: function BMT__addId(itemId) {
1414 1020
    if (this.addChangedID(GUIDForId(itemId)))
1010 1021
      this._upScore();
1022
  },
1023
1024
  /**
1025
   * Add the successor id for the item that follows the given item
1026
   */
79 1027
  _addSuccessor: function BMT__addSuccessor(itemId) {
414 1028
    let parentId = Svc.Bookmark.getFolderIdForItem(itemId);
414 1029
    let itemPos = Svc.Bookmark.getItemIndex(itemId);
621 1030
    let succId = Svc.Bookmark.getIdForItemAt(parentId, itemPos + 1);
207 1031
    if (succId != -1)
219 1032
      this._addId(succId);
1033
  },
1034
1035
  /* Every add/remove/change is worth 10 points */
212 1036
  _upScore: function BMT__upScore() {
1414 1037
    this.score += 10;
1038
  },
1039
1040
  /**
1041
   * Determine if a change should be ignored: we're ignoring everything or the
1042
   * folder is for livemarks
1043
   *
1044
   * @param itemId
1045
   *        Item under consideration to ignore
1046
   * @param folder (optional)
1047
   *        Folder of the item being changed
1048
   */
277 1049
  _ignore: function BMT__ignore(itemId, folder) {
1050
    // Ignore unconditionally if the engine tells us to
534 1051
    if (this.ignoreAll)
1052
      return true;
1053
1054
    // Ensure that the mobile bookmarks query is correct in the UI
1068 1055
    this._ensureMobileQuery();
1056
1057
    // Make sure to remove items that have the exclude annotation
1869 1058
    if (Svc.Annos.itemHasAnnotation(itemId, "places/excludeFromBackup")) {
1059
      this.removeChangedID(GUIDForId(itemId));
1060
      return true;
1061
    }
1062
1063
    // Get the folder id if we weren't given one
801 1064
    if (folder == null)
1530 1065
      folder = this._bms.getFolderIdForItem(itemId);
1066
801 1067
    let tags = kSpecialIds.tags;
1068
    // Ignore changes to tags (folders under the tags folder)
801 1069
    if (folder == tags)
1070
      return true;
1071
1072
    // Ignore tag items (the actual instance of a tag for a bookmark)
1869 1073
    if (this._bms.getFolderIdForItem(folder) == tags)
1074
      return true;
1075
1076
    // Ignore livemark children
1602 1077
    return this._ls.isLivemark(folder);
1078
  },
1079
22 1080
  onItemAdded: function BMT_onEndUpdateBatch(itemId, folder, index) {
72 1081
    if (this._ignore(itemId, folder))
1082
      return;
1083
96 1084
    this._log.trace("onItemAdded: " + itemId);
60 1085
    this._addId(itemId);
72 1086
    this._addSuccessor(itemId);
1087
  },
1088
22 1089
  onBeforeItemRemoved: function BMT_onBeforeItemRemoved(itemId) {
60 1090
    if (this._ignore(itemId))
1091
      return;
1092
96 1093
    this._log.trace("onBeforeItemRemoved: " + itemId);
60 1094
    this._addId(itemId);
72 1095
    this._addSuccessor(itemId);
1096
  },
1097
277 1098
  _ensureMobileQuery: function _ensureMobileQuery() {
534 1099
    let anno = "PlacesOrganizer/OrganizerQuery";
2937 1100
    let find = function(val) Svc.Annos.getItemsWithAnnotation(anno, {}).filter(
801 1101
      function(id) Utils.anno(id, anno) == val);
1102
1103
    // Don't continue if the Library isn't ready
1068 1104
    let all = find("AllBookmarks");
1068 1105
    if (all.length == 0)
534 1106
      return;
1107
1108
    // Disable handling of notifications while changing the mobile query
1109
    this.ignoreAll = true;
1110
1111
    let mobile = find("MobileBookmarks");
1112
    let queryURI = Utils.makeURI("place:folder=" + kSpecialIds.mobile);
1113
    let title = Str.sync.get("mobile.label");
1114
1115
    // Don't add OR do remove the mobile bookmarks if there's nothing
1116
    if (Svc.Bookmark.getIdForItemAt(kSpecialIds.mobile, 0) == -1) {
1117
      if (mobile.length != 0)
1118
        Svc.Bookmark.removeItem(mobile[0]);
1119
    }
1120
    // Add the mobile bookmarks query if it doesn't exist
1121
    else if (mobile.length == 0) {
1122
      let query = Svc.Bookmark.insertBookmark(all[0], queryURI, -1, title);
1123
      Utils.anno(query, anno, "MobileBookmarks");
1124
      Utils.anno(query, "places/excludeFromBackup", 1);
1125
    }
1126
    // Make sure the existing title is correct
1127
    else if (Svc.Bookmark.getItemTitle(mobile[0]) != title)
1128
      Svc.Bookmark.setItemTitle(mobile[0], title);
1129
1130
    this.ignoreAll = false;
1131
  },
1132
208 1133
  onItemChanged: function BMT_onItemChanged(itemId, property, isAnno, value) {
990 1134
    if (this._ignore(itemId))
1135
      return;
1136
1137
    // ignore annotations except for the ones that we sync
198 1138
    let annos = ["bookmarkProperties/description",
396 1139
      "bookmarkProperties/loadInSidebar", "bookmarks/staticTitle",
990 1140
      "livemark/feedURI", "livemark/siteURI", "microsummary/generatorURI"];
1249 1141
    if (isAnno && annos.indexOf(property) == -1)
262 1142
      return;
1143
1144
    // Ignore favicon changes to avoid unnecessary churn
201 1145
    if (property == "favicon")
1146
      return;
1147
335 1148
    this._log.trace("onItemChanged: " + itemId +
402 1149
                    (", " + property + (isAnno? " (anno)" : "")) +
646 1150
                    (value? (" = \"" + value + "\"") : ""));
402 1151
    this._addId(itemId);
1152
  },
1153
55 1154
  onItemMoved: function BMT_onItemMoved(itemId, oldParent, oldIndex, newParent, newIndex) {
225 1155
    if (this._ignore(itemId))
1156
      return;
1157
360 1158
    this._log.trace("onItemMoved: " + itemId);
225 1159
    this._addId(itemId);
225 1160
    this._addSuccessor(itemId);
1161
1162
    // Get the thing that's now at the old place
315 1163
    let oldSucc = Svc.Bookmark.getIdForItemAt(oldParent, oldIndex);
135 1164
    if (oldSucc != -1)
180 1165
      this._addId(oldSucc);
1166
1167
    // Remove any position annotations now that the user moved the item
315 1168
    Svc.Annos.removeItemAnnotation(itemId, PARENT_ANNO);
360 1169
    Svc.Annos.removeItemAnnotation(itemId, PREDECESSOR_ANNO);
1170
  },
1171
276 1172
  onBeginUpdateBatch: function BMT_onBeginUpdateBatch() {},
276 1173
  onEndUpdateBatch: function BMT_onEndUpdateBatch() {},
34 1174
  onItemRemoved: function BMT_onItemRemoved(itemId, folder, index) {},
25 1175
  onItemVisited: function BMT_onItemVisited(itemId, aVisitID, time) {}
5 1176
};