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