services/sync/modules/engines/bookmarks.js

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

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

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

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

mercurial