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