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