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