Source code

Revision control

Other Tools

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
"use strict";
6
7
var EXPORTED_SYMBOLS = ["PlacesSyncUtils"];
8
9
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
10
const { XPCOMUtils } = ChromeUtils.import(
12
);
13
14
XPCOMUtils.defineLazyGlobalGetters(this, ["URL", "URLSearchParams"]);
15
16
ChromeUtils.defineModuleGetter(this, "Log", "resource://gre/modules/Log.jsm");
17
ChromeUtils.defineModuleGetter(
18
this,
19
"PlacesUtils",
21
);
22
23
/**
24
* This module exports functions for Sync to use when applying remote
25
* records. The calls are similar to those in `Bookmarks.jsm` and
26
* `nsINavBookmarksService`, with special handling for
27
* tags, keywords, synced annotations, and missing parents.
28
*/
29
var PlacesSyncUtils = {};
30
31
const { SOURCE_SYNC } = Ci.nsINavBookmarksService;
32
33
const MICROSECONDS_PER_SECOND = 1000000;
34
35
const MOBILE_BOOKMARKS_PREF = "browser.bookmarks.showMobileBookmarks";
36
37
// These are defined as lazy getters to defer initializing the bookmarks
38
// service until it's needed.
39
XPCOMUtils.defineLazyGetter(this, "ROOT_RECORD_ID_TO_GUID", () => ({
40
menu: PlacesUtils.bookmarks.menuGuid,
41
places: PlacesUtils.bookmarks.rootGuid,
42
tags: PlacesUtils.bookmarks.tagsGuid,
43
toolbar: PlacesUtils.bookmarks.toolbarGuid,
44
unfiled: PlacesUtils.bookmarks.unfiledGuid,
45
mobile: PlacesUtils.bookmarks.mobileGuid,
46
}));
47
48
XPCOMUtils.defineLazyGetter(this, "ROOT_GUID_TO_RECORD_ID", () => ({
49
[PlacesUtils.bookmarks.menuGuid]: "menu",
50
[PlacesUtils.bookmarks.rootGuid]: "places",
51
[PlacesUtils.bookmarks.tagsGuid]: "tags",
52
[PlacesUtils.bookmarks.toolbarGuid]: "toolbar",
53
[PlacesUtils.bookmarks.unfiledGuid]: "unfiled",
54
[PlacesUtils.bookmarks.mobileGuid]: "mobile",
55
}));
56
57
XPCOMUtils.defineLazyGetter(this, "ROOTS", () =>
58
Object.keys(ROOT_RECORD_ID_TO_GUID)
59
);
60
61
const HistorySyncUtils = (PlacesSyncUtils.history = Object.freeze({
62
SYNC_ID_META_KEY: "sync/history/syncId",
63
LAST_SYNC_META_KEY: "sync/history/lastSync",
64
65
/**
66
* Returns the current history sync ID, or `""` if one isn't set.
67
*/
68
getSyncId() {
69
return PlacesUtils.metadata.get(HistorySyncUtils.SYNC_ID_META_KEY, "");
70
},
71
72
/**
73
* Assigns a new sync ID. This is called when we sync for the first time with
74
* a new account, and when we're the first to sync after a node reassignment.
75
*
76
* @return {Promise} resolved once the ID has been updated.
77
* @resolves to the new sync ID.
78
*/
79
resetSyncId() {
80
return PlacesUtils.withConnectionWrapper(
81
"HistorySyncUtils: resetSyncId",
82
function(db) {
83
let newSyncId = PlacesUtils.history.makeGuid();
84
return db.executeTransaction(async function() {
85
await setHistorySyncId(db, newSyncId);
86
return newSyncId;
87
});
88
}
89
);
90
},
91
92
/**
93
* Ensures that the existing local sync ID, if any, is up-to-date with the
94
* server. This is called when we sync with an existing account.
95
*
96
* @param newSyncId
97
* The server's sync ID.
98
* @return {Promise} resolved once the ID has been updated.
99
*/
100
async ensureCurrentSyncId(newSyncId) {
101
if (!newSyncId || typeof newSyncId != "string") {
102
throw new TypeError("Invalid new history sync ID");
103
}
104
await PlacesUtils.withConnectionWrapper(
105
"HistorySyncUtils: ensureCurrentSyncId",
106
async function(db) {
107
let existingSyncId = await PlacesUtils.metadata.getWithConnection(
108
db,
109
HistorySyncUtils.SYNC_ID_META_KEY,
110
""
111
);
112
113
if (existingSyncId == newSyncId) {
114
HistorySyncLog.trace("History sync ID up-to-date", {
115
existingSyncId,
116
});
117
return;
118
}
119
120
HistorySyncLog.info("History sync ID changed; resetting metadata", {
121
existingSyncId,
122
newSyncId,
123
});
124
await db.executeTransaction(function() {
125
return setHistorySyncId(db, newSyncId);
126
});
127
}
128
);
129
},
130
131
/**
132
* Returns the last sync time, in seconds, for the history collection, or 0
133
* if history has never synced before.
134
*/
135
async getLastSync() {
136
let lastSync = await PlacesUtils.metadata.get(
137
HistorySyncUtils.LAST_SYNC_META_KEY,
138
0
139
);
140
return lastSync / 1000;
141
},
142
143
/**
144
* Updates the history collection last sync time.
145
*
146
* @param lastSyncSeconds
147
* The collection last sync time, in seconds, as a number or string.
148
*/
149
async setLastSync(lastSyncSeconds) {
150
let lastSync = Math.floor(lastSyncSeconds * 1000);
151
if (!Number.isInteger(lastSync)) {
152
throw new TypeError("Invalid history last sync timestamp");
153
}
154
await PlacesUtils.metadata.set(
155
HistorySyncUtils.LAST_SYNC_META_KEY,
156
lastSync
157
);
158
},
159
160
/**
161
* Removes all history visits and pages from the database. Sync calls this
162
* method when it receives a command from a remote client to wipe all stored
163
* data.
164
*
165
* @return {Promise} resolved once all pages and visits have been removed.
166
*/
167
async wipe() {
168
await PlacesUtils.history.clear();
169
await HistorySyncUtils.reset();
170
},
171
172
/**
173
* Removes the sync ID and last sync time for the history collection. Unlike
174
* `wipe`, this keeps all existing history pages and visits.
175
*
176
* @return {Promise} resolved once the metadata have been removed.
177
*/
178
reset() {
179
return PlacesUtils.metadata.delete(
180
HistorySyncUtils.SYNC_ID_META_KEY,
181
HistorySyncUtils.LAST_SYNC_META_KEY
182
);
183
},
184
185
/**
186
* Clamps a history visit date between the current date and the earliest
187
* sensible date.
188
*
189
* @param {Date} visitDate
190
* The visit date.
191
* @return {Date} The clamped visit date.
192
*/
193
clampVisitDate(visitDate) {
194
let currentDate = new Date();
195
if (visitDate > currentDate) {
196
return currentDate;
197
}
198
if (visitDate < BookmarkSyncUtils.EARLIEST_BOOKMARK_TIMESTAMP) {
199
return new Date(BookmarkSyncUtils.EARLIEST_BOOKMARK_TIMESTAMP);
200
}
201
return visitDate;
202
},
203
204
/**
205
* Fetches the frecency for the URL provided
206
*
207
* @param url
208
* @returns {Number} The frecency of the given url
209
*/
210
async fetchURLFrecency(url) {
211
let canonicalURL = PlacesUtils.SYNC_BOOKMARK_VALIDATORS.url(url);
212
213
let db = await PlacesUtils.promiseDBConnection();
214
let rows = await db.executeCached(
215
`
216
SELECT frecency
217
FROM moz_places
218
WHERE url_hash = hash(:url) AND url = :url
219
LIMIT 1`,
220
{ url: canonicalURL.href }
221
);
222
223
return rows.length ? rows[0].getResultByName("frecency") : -1;
224
},
225
226
/**
227
* Filters syncable places from a collection of places guids.
228
*
229
* @param guids
230
*
231
* @returns {Array} new Array with the guids that aren't syncable
232
*/
233
async determineNonSyncableGuids(guids) {
234
// Filter out hidden pages and `TRANSITION_FRAMED_LINK` visits. These are
235
// excluded when rendering the history menu, so we use the same constraints
236
// for Sync. We also don't want to sync `TRANSITION_EMBED` visits, but those
237
// aren't stored in the database.
238
let db = await PlacesUtils.promiseDBConnection();
239
let nonSyncableGuids = [];
240
for (let chunk of PlacesUtils.chunkArray(guids, db.variableLimit)) {
241
let rows = await db.execute(
242
`
243
SELECT DISTINCT p.guid FROM moz_places p
244
JOIN moz_historyvisits v ON p.id = v.place_id
245
WHERE p.guid IN (${new Array(chunk.length).fill("?").join(",")}) AND
246
(p.hidden = 1 OR v.visit_type IN (0,
247
${PlacesUtils.history.TRANSITION_FRAMED_LINK}))
248
`,
249
chunk
250
);
251
nonSyncableGuids = nonSyncableGuids.concat(
252
rows.map(row => row.getResultByName("guid"))
253
);
254
}
255
return nonSyncableGuids;
256
},
257
258
/**
259
* Change the guid of the given uri
260
*
261
* @param uri
262
* @param guid
263
*/
264
changeGuid(uri, guid) {
265
let canonicalURL = PlacesUtils.SYNC_BOOKMARK_VALIDATORS.url(uri);
266
let validatedGuid = PlacesUtils.BOOKMARK_VALIDATORS.guid(guid);
267
return PlacesUtils.withConnectionWrapper(
268
"PlacesSyncUtils.history: changeGuid",
269
async function(db) {
270
await db.executeCached(
271
`
272
UPDATE moz_places
273
SET guid = :guid
274
WHERE url_hash = hash(:page_url) AND url = :page_url`,
275
{ guid: validatedGuid, page_url: canonicalURL.href }
276
);
277
}
278
);
279
},
280
281
/**
282
* Fetch the last 20 visits (date and type of it) corresponding to a given url
283
*
284
* @param url
285
* @returns {Array} Each element of the Array is an object with members: date and type
286
*/
287
async fetchVisitsForURL(url) {
288
let canonicalURL = PlacesUtils.SYNC_BOOKMARK_VALIDATORS.url(url);
289
let db = await PlacesUtils.promiseDBConnection();
290
let rows = await db.executeCached(
291
`
292
SELECT visit_type type, visit_date date
293
FROM moz_historyvisits
294
JOIN moz_places h ON h.id = place_id
295
WHERE url_hash = hash(:url) AND url = :url
296
ORDER BY date DESC LIMIT 20`,
297
{ url: canonicalURL.href }
298
);
299
return rows.map(row => {
300
let visitDate = row.getResultByName("date");
301
let visitType = row.getResultByName("type");
302
return { date: visitDate, type: visitType };
303
});
304
},
305
306
/**
307
* Fetches the guid of a uri
308
*
309
* @param uri
310
* @returns {String} The guid of the given uri
311
*/
312
async fetchGuidForURL(url) {
313
let canonicalURL = PlacesUtils.SYNC_BOOKMARK_VALIDATORS.url(url);
314
let db = await PlacesUtils.promiseDBConnection();
315
let rows = await db.executeCached(
316
`
317
SELECT guid
318
FROM moz_places
319
WHERE url_hash = hash(:page_url) AND url = :page_url`,
320
{ page_url: canonicalURL.href }
321
);
322
if (!rows.length) {
323
return null;
324
}
325
return rows[0].getResultByName("guid");
326
},
327
328
/**
329
* Fetch information about a guid (url, title and frecency)
330
*
331
* @param guid
332
* @returns {Object} Object with three members: url, title and frecency of the given guid
333
*/
334
async fetchURLInfoForGuid(guid) {
335
let db = await PlacesUtils.promiseDBConnection();
336
let rows = await db.executeCached(
337
`
338
SELECT url, IFNULL(title, '') AS title, frecency
339
FROM moz_places
340
WHERE guid = :guid`,
341
{ guid }
342
);
343
if (rows.length === 0) {
344
return null;
345
}
346
return {
347
url: rows[0].getResultByName("url"),
348
title: rows[0].getResultByName("title"),
349
frecency: rows[0].getResultByName("frecency"),
350
};
351
},
352
353
/**
354
* Get all URLs filtered by the limit and since members of the options object.
355
*
356
* @param options
357
* Options object with two members, since and limit. Both of them must be provided
358
* @returns {Array} - Up to limit number of URLs starting from the date provided by since
359
*/
360
async getAllURLs(options) {
361
// Check that the limit property is finite number.
362
if (!Number.isFinite(options.limit)) {
363
throw new Error("The number provided in options.limit is not finite.");
364
}
365
// Check that the since property is of type Date.
366
if (
367
!options.since ||
368
Object.prototype.toString.call(options.since) != "[object Date]"
369
) {
370
throw new Error(
371
"The property since of the options object must be of type Date."
372
);
373
}
374
let db = await PlacesUtils.promiseDBConnection();
375
let sinceInMicroseconds = PlacesUtils.toPRTime(options.since);
376
let rows = await db.executeCached(
377
`
378
SELECT DISTINCT p.url
379
FROM moz_places p
380
JOIN moz_historyvisits v ON p.id = v.place_id
381
WHERE p.last_visit_date > :cutoff_date AND
382
p.hidden = 0 AND
383
v.visit_type NOT IN (0,
384
${PlacesUtils.history.TRANSITION_FRAMED_LINK})
385
ORDER BY frecency DESC
386
LIMIT :max_results`,
387
{ cutoff_date: sinceInMicroseconds, max_results: options.limit }
388
);
389
return rows.map(row => row.getResultByName("url"));
390
},
391
}));
392
393
const BookmarkSyncUtils = (PlacesSyncUtils.bookmarks = Object.freeze({
394
SYNC_PARENT_ANNO: "sync/parent",
395
396
SYNC_ID_META_KEY: "sync/bookmarks/syncId",
397
LAST_SYNC_META_KEY: "sync/bookmarks/lastSync",
398
WIPE_REMOTE_META_KEY: "sync/bookmarks/wipeRemote",
399
400
// Jan 23, 1993 in milliseconds since 1970. Corresponds roughly to the release
401
// of the original NCSA Mosiac. We can safely assume that any dates before
402
// this time are invalid.
403
EARLIEST_BOOKMARK_TIMESTAMP: Date.UTC(1993, 0, 23),
404
405
KINDS: {
406
BOOKMARK: "bookmark",
407
QUERY: "query",
408
FOLDER: "folder",
409
LIVEMARK: "livemark",
410
SEPARATOR: "separator",
411
},
412
413
get ROOTS() {
414
return ROOTS;
415
},
416
417
/**
418
* Returns the current bookmarks sync ID, or `""` if one isn't set.
419
*/
420
getSyncId() {
421
return PlacesUtils.metadata.get(BookmarkSyncUtils.SYNC_ID_META_KEY, "");
422
},
423
424
/**
425
* Indicates if the bookmarks engine should erase all bookmarks on the server
426
* and all other clients, because the user manually restored their bookmarks
427
* from a backup on this client.
428
*/
429
async shouldWipeRemote() {
430
let shouldWipeRemote = await PlacesUtils.metadata.get(
431
BookmarkSyncUtils.WIPE_REMOTE_META_KEY,
432
false
433
);
434
return !!shouldWipeRemote;
435
},
436
437
/**
438
* Assigns a new sync ID, bumps the change counter, and flags all items as
439
* "NEW" for upload. This is called when we sync for the first time with a
440
* new account, when we're the first to sync after a node reassignment, and
441
* on the first sync after a manual restore.
442
*
443
* @return {Promise} resolved once the ID and all items have been updated.
444
* @resolves to the new sync ID.
445
*/
446
resetSyncId() {
447
return PlacesUtils.withConnectionWrapper(
448
"BookmarkSyncUtils: resetSyncId",
449
function(db) {
450
let newSyncId = PlacesUtils.history.makeGuid();
451
return db.executeTransaction(async function() {
452
await setBookmarksSyncId(db, newSyncId);
453
await resetAllSyncStatuses(db, PlacesUtils.bookmarks.SYNC_STATUS.NEW);
454
return newSyncId;
455
});
456
}
457
);
458
},
459
460
/**
461
* Ensures that the existing local sync ID, if any, is up-to-date with the
462
* server. This is called when we sync with an existing account.
463
*
464
* We always take the server's sync ID. If we don't have an existing ID,
465
* we're either syncing for the first time with an existing account, or Places
466
* has automatically restored from a backup. If the sync IDs don't match,
467
* we're likely syncing after a node reassignment, where another client
468
* uploaded their bookmarks first.
469
*
470
* @param newSyncId
471
* The server's sync ID.
472
* @return {Promise} resolved once the ID and all items have been updated.
473
*/
474
async ensureCurrentSyncId(newSyncId) {
475
if (!newSyncId || typeof newSyncId != "string") {
476
throw new TypeError("Invalid new bookmarks sync ID");
477
}
478
await PlacesUtils.withConnectionWrapper(
479
"BookmarkSyncUtils: ensureCurrentSyncId",
480
async function(db) {
481
let existingSyncId = await PlacesUtils.metadata.getWithConnection(
482
db,
483
BookmarkSyncUtils.SYNC_ID_META_KEY,
484
""
485
);
486
487
// If we don't have a sync ID, take the server's without resetting
488
// sync statuses.
489
if (!existingSyncId) {
490
BookmarkSyncLog.info("Taking new bookmarks sync ID", { newSyncId });
491
await db.executeTransaction(() => setBookmarksSyncId(db, newSyncId));
492
return;
493
}
494
495
// If the existing sync ID matches the server, great!
496
if (existingSyncId == newSyncId) {
497
BookmarkSyncLog.trace("Bookmarks sync ID up-to-date", {
498
existingSyncId,
499
});
500
return;
501
}
502
503
// Otherwise, we have a sync ID, but it doesn't match, so we were likely
504
// node reassigned. Take the server's sync ID and reset all items to
505
// "UNKNOWN" so that we can merge.
506
BookmarkSyncLog.info(
507
"Bookmarks sync ID changed; resetting sync statuses",
508
{ existingSyncId, newSyncId }
509
);
510
await db.executeTransaction(async function() {
511
await setBookmarksSyncId(db, newSyncId);
512
await resetAllSyncStatuses(
513
db,
514
PlacesUtils.bookmarks.SYNC_STATUS.UNKNOWN
515
);
516
});
517
}
518
);
519
},
520
521
/**
522
* Returns the last sync time, in seconds, for the bookmarks collection, or 0
523
* if bookmarks have never synced before.
524
*/
525
async getLastSync() {
526
let lastSync = await PlacesUtils.metadata.get(
527
BookmarkSyncUtils.LAST_SYNC_META_KEY,
528
0
529
);
530
return lastSync / 1000;
531
},
532
533
/**
534
* Updates the bookmarks collection last sync time.
535
*
536
* @param lastSyncSeconds
537
* The collection last sync time, in seconds, as a number or string.
538
*/
539
async setLastSync(lastSyncSeconds) {
540
let lastSync = Math.floor(lastSyncSeconds * 1000);
541
if (!Number.isInteger(lastSync)) {
542
throw new TypeError("Invalid bookmarks last sync timestamp");
543
}
544
await PlacesUtils.metadata.set(
545
BookmarkSyncUtils.LAST_SYNC_META_KEY,
546
lastSync
547
);
548
},
549
550
/**
551
* Resets Sync metadata for bookmarks in Places. This function behaves
552
* differently depending on the change source, and may be called from
553
* `PlacesSyncUtils.bookmarks.reset` or
554
* `PlacesUtils.bookmarks.eraseEverything`.
555
*
556
* - RESTORE: The user is restoring from a backup. Drop the sync ID, last
557
* sync time, and tombstones; reset sync statuses for remaining items to
558
* "NEW"; then set a flag to wipe the server and all other clients. On the
559
* next sync, we'll replace their bookmarks with ours.
560
*
561
* - RESTORE_ON_STARTUP: Places is automatically restoring from a backup to
562
* recover from a corrupt database. The sync ID, last sync time, and
563
* tombstones don't exist, since we don't back them up; reset sync statuses
564
* for the roots to "UNKNOWN"; but don't wipe the server. On the next sync,
565
* we'll merge the restored bookmarks with the ones on the server.
566
*
567
* - SYNC: Either another client told us to erase our bookmarks
568
* (`PlacesSyncUtils.bookmarks.wipe`), or the user disconnected Sync
569
* (`PlacesSyncUtils.bookmarks.reset`). In both cases, drop the existing
570
* sync ID, last sync time, and tombstones; reset sync statuses for
571
* remaining items to "NEW"; and don't wipe the server.
572
*
573
* @param db
574
* the Sqlite.jsm connection handle.
575
* @param source
576
* the change source constant.
577
*/
578
async resetSyncMetadata(db, source) {
579
if (
580
![
581
PlacesUtils.bookmarks.SOURCES.RESTORE,
582
PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
583
PlacesUtils.bookmarks.SOURCES.SYNC,
584
].includes(source)
585
) {
586
return;
587
}
588
589
// Remove the sync ID and last sync time in all cases.
590
await PlacesUtils.metadata.deleteWithConnection(
591
db,
592
BookmarkSyncUtils.SYNC_ID_META_KEY,
593
BookmarkSyncUtils.LAST_SYNC_META_KEY
594
);
595
596
// If we're manually restoring from a backup, wipe the server and other
597
// clients, so that we replace their bookmarks with the restored tree. If
598
// we're automatically restoring to recover from a corrupt database, don't
599
// wipe; we want to merge the restored tree with the one on the server.
600
await PlacesUtils.metadata.setWithConnection(
601
db,
602
BookmarkSyncUtils.WIPE_REMOTE_META_KEY,
603
source == PlacesUtils.bookmarks.SOURCES.RESTORE
604
);
605
606
// Reset change counters and sync statuses for roots and remaining
607
// items, and drop tombstones.
608
let syncStatus =
609
source == PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP
610
? PlacesUtils.bookmarks.SYNC_STATUS.UNKNOWN
611
: PlacesUtils.bookmarks.SYNC_STATUS.NEW;
612
await resetAllSyncStatuses(db, syncStatus);
613
},
614
615
/**
616
* Converts a Places GUID to a Sync record ID. Record IDs are identical to
617
* Places GUIDs for all items except roots.
618
*/
619
guidToRecordId(guid) {
620
return ROOT_GUID_TO_RECORD_ID[guid] || guid;
621
},
622
623
/**
624
* Converts a Sync record ID to a Places GUID.
625
*/
626
recordIdToGuid(recordId) {
627
return ROOT_RECORD_ID_TO_GUID[recordId] || recordId;
628
},
629
630
/**
631
* Fetches the record IDs for a folder's children, ordered by their position
632
* within the folder.
633
*/
634
fetchChildRecordIds(parentRecordId) {
635
PlacesUtils.SYNC_BOOKMARK_VALIDATORS.recordId(parentRecordId);
636
let parentGuid = BookmarkSyncUtils.recordIdToGuid(parentRecordId);
637
638
return PlacesUtils.withConnectionWrapper(
639
"BookmarkSyncUtils: fetchChildRecordIds",
640
async function(db) {
641
let childGuids = await fetchChildGuids(db, parentGuid);
642
return childGuids.map(guid => BookmarkSyncUtils.guidToRecordId(guid));
643
}
644
);
645
},
646
647
/**
648
* Returns an array of `{ recordId, syncable }` tuples for all items in
649
* `requestedRecordIds`. If any requested ID is a folder, all its descendants
650
* will be included. Ancestors of non-syncable items are not included; if
651
* any are missing on the server, the requesting client will need to make
652
* another repair request.
653
*
654
* Sync calls this method to respond to incoming bookmark repair requests
655
* and upload items that are missing on the server.
656
*/
657
fetchRecordIdsForRepair(requestedRecordIds) {
658
let requestedGuids = requestedRecordIds.map(
659
BookmarkSyncUtils.recordIdToGuid
660
);
661
return PlacesUtils.withConnectionWrapper(
662
"BookmarkSyncUtils: fetchRecordIdsForRepair",
663
async function(db) {
664
let rows = await db.executeCached(`
665
WITH RECURSIVE
666
syncedItems(id) AS (
667
SELECT b.id FROM moz_bookmarks b
668
WHERE b.guid IN ('menu________', 'toolbar_____', 'unfiled_____',
669
'mobile______')
670
UNION ALL
671
SELECT b.id FROM moz_bookmarks b
672
JOIN syncedItems s ON b.parent = s.id
673
),
674
descendants(id) AS (
675
SELECT b.id FROM moz_bookmarks b
676
WHERE b.guid IN (${requestedGuids
677
.map(guid => JSON.stringify(guid))
678
.join(",")})
679
UNION ALL
680
SELECT b.id FROM moz_bookmarks b
681
JOIN descendants d ON d.id = b.parent
682
)
683
SELECT b.guid, s.id NOT NULL AS syncable
684
FROM descendants d
685
JOIN moz_bookmarks b ON b.id = d.id
686
LEFT JOIN syncedItems s ON s.id = d.id
687
`);
688
return rows.map(row => {
689
let recordId = BookmarkSyncUtils.guidToRecordId(
690
row.getResultByName("guid")
691
);
692
let syncable = !!row.getResultByName("syncable");
693
return { recordId, syncable };
694
});
695
}
696
);
697
},
698
699
/**
700
* Migrates an array of `{ recordId, modified }` tuples from the old JSON-based
701
* tracker to the new sync change counter. `modified` is when the change was
702
* added to the old tracker, in milliseconds.
703
*
704
* Sync calls this method before the first bookmark sync after the Places
705
* schema migration.
706
*/
707
migrateOldTrackerEntries(entries) {
708
return PlacesUtils.withConnectionWrapper(
709
"BookmarkSyncUtils: migrateOldTrackerEntries",
710
function(db) {
711
return db.executeTransaction(async function() {
712
// Mark all existing bookmarks as synced, and clear their change
713
// counters to avoid a full upload on the next sync. Note that
714
// this means we'll miss changes made between startup and the first
715
// post-migration sync, as well as changes made on a new release
716
// channel that weren't synced before the user downgraded. This is
717
// unfortunate, but no worse than the behavior of the old tracker.
718
//
719
// We also likely have bookmarks that don't exist on the server,
720
// because the old tracker missed them. We'll eventually fix the
721
// server once we decide on a repair strategy.
722
await db.executeCached(
723
`
724
WITH RECURSIVE
725
syncedItems(id) AS (
726
SELECT b.id FROM moz_bookmarks b
727
WHERE b.guid IN ('menu________', 'toolbar_____', 'unfiled_____',
728
'mobile______')
729
UNION ALL
730
SELECT b.id FROM moz_bookmarks b
731
JOIN syncedItems s ON b.parent = s.id
732
)
733
UPDATE moz_bookmarks SET
734
syncStatus = :syncStatus,
735
syncChangeCounter = 0
736
WHERE id IN syncedItems`,
737
{ syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL }
738
);
739
740
await db.executeCached(`DELETE FROM moz_bookmarks_deleted`);
741
742
await db.executeCached(`CREATE TEMP TABLE moz_bookmarks_tracked (
743
guid TEXT PRIMARY KEY,
744
time INTEGER
745
)`);
746
747
try {
748
for (let { recordId, modified } of entries) {
749
let guid = BookmarkSyncUtils.recordIdToGuid(recordId);
750
if (!PlacesUtils.isValidGuid(guid)) {
751
BookmarkSyncLog.warn(
752
`migrateOldTrackerEntries: Ignoring ` +
753
`change for invalid item ${guid}`
754
);
755
continue;
756
}
757
let time = PlacesUtils.toPRTime(
758
Number.isFinite(modified) ? modified : Date.now()
759
);
760
await db.executeCached(
761
`
762
INSERT OR IGNORE INTO moz_bookmarks_tracked (guid, time)
763
VALUES (:guid, :time)`,
764
{ guid, time }
765
);
766
}
767
768
// Bump the change counter for existing tracked items.
769
await db.executeCached(`
770
INSERT OR REPLACE INTO moz_bookmarks (id, fk, type, parent,
771
position, title,
772
dateAdded, lastModified,
773
guid, syncChangeCounter,
774
syncStatus)
775
SELECT b.id, b.fk, b.type, b.parent, b.position, b.title,
776
b.dateAdded, MAX(b.lastModified, t.time), b.guid,
777
b.syncChangeCounter + 1, b.syncStatus
778
FROM moz_bookmarks b
779
JOIN moz_bookmarks_tracked t ON b.guid = t.guid`);
780
781
// Insert tombstones for nonexistent tracked items, using the most
782
// recent deletion date for more accurate reconciliation. We assume
783
// the tracked item belongs to a synced root.
784
await db.executeCached(`
785
INSERT OR REPLACE INTO moz_bookmarks_deleted (guid, dateRemoved)
786
SELECT t.guid, MAX(IFNULL((SELECT dateRemoved FROM moz_bookmarks_deleted
787
WHERE guid = t.guid), 0), t.time)
788
FROM moz_bookmarks_tracked t
789
LEFT JOIN moz_bookmarks b ON t.guid = b.guid
790
WHERE b.guid IS NULL`);
791
} finally {
792
await db.executeCached(`DROP TABLE moz_bookmarks_tracked`);
793
}
794
});
795
}
796
);
797
},
798
799
/**
800
* Reorders a folder's children, based on their order in the array of sync
801
* IDs.
802
*
803
* Sync uses this method to reorder all synced children after applying all
804
* incoming records.
805
*
806
* @return {Promise} resolved when reordering is complete.
807
* @rejects if an error happens while reordering.
808
* @throws if the arguments are invalid.
809
*/
810
order(parentRecordId, childRecordIds) {
811
PlacesUtils.SYNC_BOOKMARK_VALIDATORS.recordId(parentRecordId);
812
if (!childRecordIds.length) {
813
return undefined;
814
}
815
let parentGuid = BookmarkSyncUtils.recordIdToGuid(parentRecordId);
816
if (parentGuid == PlacesUtils.bookmarks.rootGuid) {
817
// Reordering roots doesn't make sense, but Sync will do this on the
818
// first sync.
819
return undefined;
820
}
821
let orderedChildrenGuids = childRecordIds.map(
822
BookmarkSyncUtils.recordIdToGuid
823
);
824
return PlacesUtils.bookmarks.reorder(parentGuid, orderedChildrenGuids, {
825
source: SOURCE_SYNC,
826
});
827
},
828
829
/**
830
* Resolves to true if there are known sync changes.
831
*/
832
havePendingChanges() {
833
return PlacesUtils.withConnectionWrapper(
834
"BookmarkSyncUtils: havePendingChanges",
835
async function(db) {
836
let rows = await db.executeCached(`
837
WITH RECURSIVE
838
syncedItems(id, guid, syncChangeCounter) AS (
839
SELECT b.id, b.guid, b.syncChangeCounter
840
FROM moz_bookmarks b
841
WHERE b.guid IN ('menu________', 'toolbar_____', 'unfiled_____',
842
'mobile______')
843
UNION ALL
844
SELECT b.id, b.guid, b.syncChangeCounter
845
FROM moz_bookmarks b
846
JOIN syncedItems s ON b.parent = s.id
847
),
848
changedItems(guid) AS (
849
SELECT guid FROM syncedItems
850
WHERE syncChangeCounter >= 1
851
UNION ALL
852
SELECT guid FROM moz_bookmarks_deleted
853
)
854
SELECT EXISTS(SELECT guid FROM changedItems) AS haveChanges`);
855
return !!rows[0].getResultByName("haveChanges");
856
}
857
);
858
},
859
860
/**
861
* Returns a changeset containing local bookmark changes since the last sync.
862
*
863
* @return {Promise} resolved once all items have been fetched.
864
* @resolves to an object containing records for changed bookmarks, keyed by
865
* the record ID.
866
* @see pullSyncChanges for the implementation, and markChangesAsSyncing for
867
* an explanation of why we update the sync status.
868
*/
869
pullChanges() {
870
return PlacesUtils.withConnectionWrapper(
871
"BookmarkSyncUtils: pullChanges",
872
pullSyncChanges
873
);
874
},
875
876
/**
877
* Updates the sync status of all "NEW" bookmarks to "NORMAL", so that Sync
878
* can recover correctly after an interrupted sync.
879
*
880
* @param changeRecords
881
* A changeset containing sync change records, as returned by
882
* `pullChanges`.
883
* @return {Promise} resolved once all records have been updated.
884
*/
885
markChangesAsSyncing(changeRecords) {
886
return PlacesUtils.withConnectionWrapper(
887
"BookmarkSyncUtils: markChangesAsSyncing",
888
db => markChangesAsSyncing(db, changeRecords)
889
);
890
},
891
892
/**
893
* Decrements the sync change counter, updates the sync status, and cleans up
894
* tombstones for successfully synced items. Sync calls this method at the
895
* end of each bookmark sync.
896
*
897
* @param changeRecords
898
* A changeset containing sync change records, as returned by
899
* `pullChanges`.
900
* @return {Promise} resolved once all records have been updated.
901
*/
902
pushChanges(changeRecords) {
903
return PlacesUtils.withConnectionWrapper(
904
"BookmarkSyncUtils: pushChanges",
905
async function(db) {
906
let skippedCount = 0;
907
let weakCount = 0;
908
let updateParams = [];
909
let tombstoneGuidsToRemove = [];
910
911
for (let recordId in changeRecords) {
912
// Validate change records to catch coding errors.
913
let changeRecord = validateChangeRecord(
914
"BookmarkSyncUtils: pushChanges",
915
changeRecords[recordId],
916
{
917
tombstone: { required: true },
918
counter: { required: true },
919
synced: { required: true },
920
}
921
);
922
923
// Skip weakly uploaded records.
924
if (!changeRecord.counter) {
925
weakCount++;
926
continue;
927
}
928
929
// Sync sets the `synced` flag for reconciled or successfully
930
// uploaded items. If upload failed, ignore the change; we'll
931
// try again on the next sync.
932
if (!changeRecord.synced) {
933
skippedCount++;
934
continue;
935
}
936
937
let guid = BookmarkSyncUtils.recordIdToGuid(recordId);
938
if (changeRecord.tombstone) {
939
tombstoneGuidsToRemove.push(guid);
940
} else {
941
updateParams.push({
942
guid,
943
syncChangeDelta: changeRecord.counter,
944
syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
945
});
946
}
947
}
948
949
// Reduce the change counter and update the sync status for
950
// reconciled and uploaded items. If the bookmark was updated
951
// during the sync, its change counter will still be > 0 for the
952
// next sync.
953
if (updateParams.length || tombstoneGuidsToRemove.length) {
954
await db.executeTransaction(async function() {
955
if (updateParams.length) {
956
await db.executeCached(
957
`
958
UPDATE moz_bookmarks
959
SET syncChangeCounter = MAX(syncChangeCounter - :syncChangeDelta, 0),
960
syncStatus = :syncStatus
961
WHERE guid = :guid`,
962
updateParams
963
);
964
// and if there are *both* bookmarks and tombstones for these
965
// items, we nuke the tombstones.
966
// This should be unlikely, but bad if it happens.
967
let dupedGuids = updateParams.map(({ guid }) => guid);
968
await removeUndeletedTombstones(db, dupedGuids);
969
}
970
await removeTombstones(db, tombstoneGuidsToRemove);
971
});
972
}
973
974
BookmarkSyncLog.debug(`pushChanges: Processed change records`, {
975
weak: weakCount,
976
skipped: skippedCount,
977
updated: updateParams.length,
978
});
979
}
980
);
981
},
982
983
/**
984
* Removes items from the database. Sync buffers incoming tombstones, and
985
* calls this method to apply them at the end of each sync. Deletion
986
* happens in three steps:
987
*
988
* 1. Remove all non-folder items. Deleting a folder on a remote client
989
* uploads tombstones for the folder and its children at the time of
990
* deletion. This preserves any new children we've added locally since
991
* the last sync.
992
* 2. Reparent remaining children to the tombstoned folder's parent. This
993
* bumps the change counter for the children and their new parent.
994
* 3. Remove the tombstoned folder. Because we don't do this in a
995
* transaction, the user might move new items into the folder before we
996
* can remove it. In that case, we keep the folder and upload the new
997
* subtree to the server.
998
*
999
* See the comment above `BookmarksStore::deletePending` for the details on
1000
* why delete works the way it does.
1001
*/
1002
remove(recordIds) {
1003
if (!recordIds.length) {
1004
return null;
1005
}
1006
1007
return PlacesUtils.withConnectionWrapper(
1008
"BookmarkSyncUtils: remove",
1009
async function(db) {
1010
let folderGuids = [];
1011
for (let recordId of recordIds) {
1012
if (recordId in ROOT_RECORD_ID_TO_GUID) {
1013
BookmarkSyncLog.warn(`remove: Refusing to remove root ${recordId}`);
1014
continue;
1015
}
1016
let guid = BookmarkSyncUtils.recordIdToGuid(recordId);
1017
let bookmarkItem = await PlacesUtils.bookmarks.fetch(guid);
1018
if (!bookmarkItem) {
1019
BookmarkSyncLog.trace(`remove: Item ${guid} already removed`);
1020
continue;
1021
}
1022
let kind = await getKindForItem(db, bookmarkItem);
1023
if (kind == BookmarkSyncUtils.KINDS.FOLDER) {
1024
folderGuids.push(bookmarkItem.guid);
1025
continue;
1026
}
1027
let wasRemoved = await deleteSyncedAtom(bookmarkItem);
1028
if (wasRemoved) {
1029
BookmarkSyncLog.trace(
1030
`remove: Removed item ${guid} with kind ${kind}`
1031
);
1032
}
1033
}
1034
1035
for (let guid of folderGuids) {
1036
let bookmarkItem = await PlacesUtils.bookmarks.fetch(guid);
1037
if (!bookmarkItem) {
1038
BookmarkSyncLog.trace(`remove: Folder ${guid} already removed`);
1039
continue;
1040
}
1041
let wasRemoved = await deleteSyncedFolder(db, bookmarkItem);
1042
if (wasRemoved) {
1043
BookmarkSyncLog.trace(
1044
`remove: Removed folder ${bookmarkItem.guid}`
1045
);
1046
}
1047
}
1048
1049
// TODO (Bug 1313890): Refactor the bookmarks engine to pull change records
1050
// before uploading, instead of returning records to merge into the engine's
1051
// initial changeset.
1052
return pullSyncChanges(db);
1053
}
1054
);
1055
},
1056
1057
/**
1058
* Increments the change counter of a non-folder item and its parent. Sync
1059
* calls this method to override a remote deletion for an item that's changed
1060
* locally.
1061
*
1062
* @param recordId
1063
* The record ID to revive.
1064
* @return {Promise} resolved once the change counters have been updated.
1065
* @resolves to `null` if the item doesn't exist or is a folder. Otherwise,
1066
* resolves to an object containing new change records for the item
1067
* and its parent. The bookmarks engine merges these records into
1068
* the changeset for the current sync.
1069
*/
1070
async touch(recordId) {
1071
PlacesUtils.SYNC_BOOKMARK_VALIDATORS.recordId(recordId);
1072
let guid = BookmarkSyncUtils.recordIdToGuid(recordId);
1073
1074
let bookmarkItem = await PlacesUtils.bookmarks.fetch(guid);
1075
if (!bookmarkItem) {
1076
return null;
1077
}
1078
return PlacesUtils.withConnectionWrapper(
1079
"BookmarkSyncUtils: touch",
1080
async function(db) {
1081
let kind = await getKindForItem(db, bookmarkItem);
1082
if (kind == BookmarkSyncUtils.KINDS.FOLDER) {
1083
// We avoid reviving folders since reviving them properly would require
1084
// reviving their children as well. Unfortunately, this is the wrong
1085
// choice in the case of a bookmark restore where the bookmarks engine
1086
// fails to wipe the server. In that case, if the server has the folder
1087
// as deleted, we *would* want to reupload this folder. This is mitigated
1088
// by the fact that `remove` moves any undeleted children to the
1089
// grandparent when deleting the parent.
1090
return null;
1091
}
1092
return touchSyncBookmark(db, bookmarkItem);
1093
}
1094
);
1095
},
1096
1097
/**
1098
* Returns true for record IDs that are considered roots.
1099
*/
1100
isRootRecordID(id) {
1101
return ROOT_RECORD_ID_TO_GUID.hasOwnProperty(id);
1102
},
1103
1104
/**
1105
* Removes all bookmarks and tombstones from the database. Sync calls this
1106
* method when it receives a command from a remote client to wipe all stored
1107
* data.
1108
*
1109
* @return {Promise} resolved once all items have been removed.
1110
*/
1111
wipe() {
1112
return PlacesUtils.bookmarks.eraseEverything({
1113
source: SOURCE_SYNC,
1114
});
1115
},
1116
1117
/**
1118
* Marks all bookmarks as "NEW" and removes all tombstones. Unlike `wipe`,
1119
* this keeps all existing bookmarks, and only clears their sync change
1120
* tracking info.
1121
*
1122
* @return {Promise} resolved once all items have been updated.
1123
*/
1124
reset() {
1125
return PlacesUtils.withConnectionWrapper(
1126
"BookmarkSyncUtils: reset",
1127
function(db) {
1128
return db.executeTransaction(async function() {
1129
await BookmarkSyncUtils.resetSyncMetadata(db, SOURCE_SYNC);
1130
});
1131
}
1132
);
1133
},
1134
1135
/**
1136
* De-dupes an item by changing its record ID to match the ID on the server.
1137
* Sync calls this method when it detects an incoming item is a duplicate of
1138
* an existing local item.
1139
*
1140
* Note that this method doesn't move the item if the local and remote sync
1141
* IDs are different. That happens after de-duping, when the bookmarks engine
1142
* calls `update` to update the item.
1143
*
1144
* @param localRecordId
1145
* The local ID to change.
1146
* @param remoteRecordId
1147
* The remote ID that should replace the local ID.
1148
* @param remoteParentRecordId
1149
* The remote record's parent ID.
1150
* @return {Promise} resolved once the ID has been changed.
1151
* @resolves to an object containing new change records for the old item,
1152
* the local parent, and the remote parent if different from the
1153
* local parent. The bookmarks engine merges these records into the
1154
* changeset for the current sync.
1155
*/
1156
dedupe(localRecordId, remoteRecordId, remoteParentRecordId) {
1157
PlacesUtils.SYNC_BOOKMARK_VALIDATORS.recordId(localRecordId);
1158
PlacesUtils.SYNC_BOOKMARK_VALIDATORS.recordId(remoteRecordId);
1159
PlacesUtils.SYNC_BOOKMARK_VALIDATORS.recordId(remoteParentRecordId);
1160
1161
return PlacesUtils.withConnectionWrapper("BookmarkSyncUtils: dedupe", db =>
1162
dedupeSyncBookmark(
1163
db,
1164
BookmarkSyncUtils.recordIdToGuid(localRecordId),
1165
BookmarkSyncUtils.recordIdToGuid(remoteRecordId),
1166
BookmarkSyncUtils.recordIdToGuid(remoteParentRecordId)
1167
)
1168
);
1169
},
1170
1171
/**
1172
* Updates a bookmark with synced properties. Only Sync should call this
1173
* method; other callers should use `Bookmarks.update`.
1174
*
1175
* The following properties are supported:
1176
* - kind: Optional.
1177
* - guid: Required.
1178
* - parentGuid: Optional; reparents the bookmark if specified.
1179
* - title: Optional.
1180
* - url: Optional.
1181
* - tags: Optional; replaces all existing tags.
1182
* - keyword: Optional.
1183
* - query: Optional.
1184
*
1185
* @param info
1186
* object representing a bookmark-item, as defined above.
1187
*
1188
* @return {Promise} resolved when the update is complete.
1189
* @resolves to an object representing the updated bookmark.
1190
* @rejects if it's not possible to update the given bookmark.
1191
* @throws if the arguments are invalid.
1192
*/
1193
update(info) {
1194
let updateInfo = validateSyncBookmarkObject(
1195
"BookmarkSyncUtils: update",
1196
info,
1197
{ recordId: { required: true } }
1198
);
1199
1200
return PlacesUtils.withConnectionWrapper("BookmarkSyncUtils: update", db =>
1201
updateSyncBookmark(db, updateInfo)
1202
);
1203
},
1204
1205
/**
1206
* Inserts a synced bookmark into the tree. Only Sync should call this
1207
* method; other callers should use `Bookmarks.insert`.
1208
*
1209
* The following properties are supported:
1210
* - kind: Required.
1211
* - guid: Required.
1212
* - parentGuid: Required.
1213
* - url: Required for bookmarks.
1214
* - tags: An optional array of tag strings.
1215
* - keyword: An optional keyword string.
1216
*
1217
* Sync doesn't set the index, since it appends and reorders children
1218
* after applying all incoming items.
1219
*
1220
* @param info
1221
* object representing a synced bookmark.
1222
*
1223
* @return {Promise} resolved when the creation is complete.
1224
* @resolves to an object representing the created bookmark.
1225
* @rejects if it's not possible to create the requested bookmark.
1226
* @throws if the arguments are invalid.
1227
*/
1228
insert(info) {
1229
let insertInfo = validateNewBookmark("BookmarkSyncUtils: insert", info);
1230
1231
return PlacesUtils.withConnectionWrapper("BookmarkSyncUtils: insert", db =>
1232
insertSyncBookmark(db, insertInfo)
1233
);
1234
},
1235
1236
/**
1237
* Fetches a Sync bookmark object for an item in the tree. The object contains
1238
* the following properties, depending on the item's kind:
1239
*
1240
* - kind (all): A string representing the item's kind.
1241
* - recordId (all): The item's record ID.
1242
* - parentRecordId (all): The record ID of the item's parent.
1243
* - parentTitle (all): The title of the item's parent, used for de-duping.
1244
* Omitted for the Places root and parents with empty titles.
1245
* - dateAdded (all): Timestamp in milliseconds, when the bookmark was added
1246
* or created on a remote device if known.
1247
* - title ("bookmark", "folder", "query"): The item's title.
1248
* Omitted if empty.
1249
* - url ("bookmark", "query"): The item's URL.
1250
* - tags ("bookmark", "query"): An array containing the item's tags.
1251
* - keyword ("bookmark"): The bookmark's keyword, if one exists.
1252
* - childRecordIds ("folder"): An array containing the record IDs of the item's
1253
* children, used to determine child order.
1254
* - folder ("query"): The tag folder name, if this is a tag query.
1255
* - index ("separator"): The separator's position within its parent.
1256
*/
1257
async fetch(recordId) {
1258
let guid = BookmarkSyncUtils.recordIdToGuid(recordId);
1259
let bookmarkItem = await PlacesUtils.bookmarks.fetch(guid);
1260
if (!bookmarkItem) {
1261
return null;
1262
}
1263
return PlacesUtils.withConnectionWrapper(
1264
"BookmarkSyncUtils: fetch",
1265
async function(db) {
1266
// Convert the Places bookmark object to a Sync bookmark and add
1267
// kind-specific properties. Titles are required for bookmarks,
1268
// and folders; optional for queries, and omitted for separators.
1269
let kind = await getKindForItem(db, bookmarkItem);
1270
let item;
1271
switch (kind) {
1272
case BookmarkSyncUtils.KINDS.BOOKMARK:
1273
item = await fetchBookmarkItem(db, bookmarkItem);
1274
break;
1275
1276
case BookmarkSyncUtils.KINDS.QUERY:
1277
item = await fetchQueryItem(db, bookmarkItem);
1278
break;
1279
1280
case BookmarkSyncUtils.KINDS.FOLDER:
1281
item = await fetchFolderItem(db, bookmarkItem);
1282
break;
1283
1284
case BookmarkSyncUtils.KINDS.SEPARATOR:
1285
item = await placesBookmarkToSyncBookmark(db, bookmarkItem);
1286
item.index = bookmarkItem.index;
1287
break;
1288
1289
default:
1290
throw new Error(`Unknown bookmark kind: ${kind}`);
1291
}
1292
1293
// Sync uses the parent title for de-duping. All Sync bookmark objects
1294
// except the Places root should have this property.
1295
if (bookmarkItem.parentGuid) {
1296
let parent = await PlacesUtils.bookmarks.fetch(
1297
bookmarkItem.parentGuid
1298
);
1299
item.parentTitle = parent.title || "";
1300
}
1301
1302
return item;
1303
}
1304
);
1305
},
1306
1307
/**
1308
* Returns the sync change counter increment for a change source constant.
1309
*/
1310
determineSyncChangeDelta(source) {
1311
// Don't bump the change counter when applying changes made by Sync, to
1312
// avoid sync loops.
1313
return source == PlacesUtils.bookmarks.SOURCES.SYNC ? 0 : 1;
1314
},
1315
1316
/**
1317
* Returns the sync status for a new item inserted by a change source.
1318
*/
1319
determineInitialSyncStatus(source) {
1320
if (source == PlacesUtils.bookmarks.SOURCES.SYNC) {
1321
// Incoming bookmarks are "NORMAL", since they already exist on the server.
1322
return PlacesUtils.bookmarks.SYNC_STATUS.NORMAL;
1323
}
1324
if (source == PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP) {
1325
// If the user restores from a backup, or Places automatically recovers
1326
// from a corrupt database, all prior sync tracking is lost. Setting the
1327
// status to "UNKNOWN" allows Sync to reconcile restored bookmarks with
1328
// those on the server.
1329
return PlacesUtils.bookmarks.SYNC_STATUS.UNKNOWN;
1330
}
1331
// For all other sources, mark items as "NEW". We'll update their statuses
1332
// to "NORMAL" after the first sync.
1333
return PlacesUtils.bookmarks.SYNC_STATUS.NEW;
1334
},
1335
1336
/**
1337
* An internal helper that bumps the change counter for all bookmarks with
1338
* a given URL. This is used to update bookmarks when adding or changing a
1339
* tag or keyword entry.
1340
*
1341
* @param db
1342
* the Sqlite.jsm connection handle.
1343
* @param url
1344
* the bookmark URL object.
1345
* @param syncChangeDelta
1346
* the sync change counter increment.
1347
* @return {Promise} resolved when the counters have been updated.
1348
*/
1349
addSyncChangesForBookmarksWithURL(db, url, syncChangeDelta) {
1350
if (!url || !syncChangeDelta) {
1351
return Promise.resolve();
1352
}
1353
return db.executeCached(
1354
`
1355
UPDATE moz_bookmarks
1356
SET syncChangeCounter = syncChangeCounter + :syncChangeDelta
1357
WHERE type = :type AND
1358
fk = (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND
1359
url = :url)`,
1360
{
1361
syncChangeDelta,
1362
type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
1363
url: url.href,
1364
}
1365
);
1366
},
1367
1368
async removeLivemark(livemarkInfo) {
1369
let info = validateSyncBookmarkObject(
1370
"BookmarkSyncUtils: removeLivemark",
1371
livemarkInfo,
1372
{
1373
kind: {
1374
required: true,
1375
validIf: b => b.kind == BookmarkSyncUtils.KINDS.LIVEMARK,
1376
},
1377
recordId: { required: true },
1378
parentRecordId: { required: true },
1379
}
1380
);
1381
1382
let guid = BookmarkSyncUtils.recordIdToGuid(info.recordId);
1383
let parentGuid = BookmarkSyncUtils.recordIdToGuid(info.parentRecordId);
1384
1385
return PlacesUtils.withConnectionWrapper(
1386
"BookmarkSyncUtils: removeLivemark",
1387
async function(db) {
1388
if (await GUIDMissing(guid)) {
1389
// If the livemark doesn't exist in the database, insert a tombstone
1390
// and bump its parent's change counter to ensure it's removed from
1391
// the server in the current sync.
1392
await db.executeTransaction(async function() {
1393
await db.executeCached(
1394
`
1395
UPDATE moz_bookmarks SET
1396
syncChangeCounter = syncChangeCounter + 1
1397
WHERE guid = :parentGuid`,
1398
{ parentGuid }
1399
);
1400
1401
await db.executeCached(
1402
`
1403
INSERT OR IGNORE INTO moz_bookmarks_deleted (guid, dateRemoved)
1404
VALUES (:guid, ${PlacesUtils.toPRTime(Date.now())})`,
1405
{ guid }
1406
);
1407
});
1408
} else {
1409
await PlacesUtils.bookmarks.remove({
1410
guid,
1411
// `SYNC_REPARENT_REMOVED_FOLDER_CHILDREN` bumps the change counter for
1412
// the child and its new parent, without incrementing the bookmark
1413
// tracker's score.
1414
source:
1415
PlacesUtils.bookmarks.SOURCES
1416
.SYNC_REPARENT_REMOVED_FOLDER_CHILDREN,
1417
});
1418
}
1419
1420
return pullSyncChanges(db, [guid, parentGuid]);
1421
}
1422
);
1423
},
1424
1425
/**
1426
* Returns `0` if no sensible timestamp could be found.
1427
* Otherwise, returns the earliest sensible timestamp between `existingMillis`
1428
* and `serverMillis`.
1429
*/
1430
ratchetTimestampBackwards(
1431
existingMillis,
1432
serverMillis,
1433
lowerBound = BookmarkSyncUtils.EARLIEST_BOOKMARK_TIMESTAMP
1434
) {
1435
const possible = [+existingMillis, +serverMillis].filter(
1436
n => !isNaN(n) && n > lowerBound
1437
);
1438
if (!possible.length) {
1439
return 0;
1440
}
1441
return Math.min(...possible);
1442
},
1443
1444
/**
1445
* Rebuilds the left pane query for the mobile root under "All Bookmarks" if
1446
* necessary. Sync calls this method at the end of each bookmark sync. This
1447
* code should eventually move to `PlacesUIUtils#maybeRebuildLeftPane`; see
1448
* bug 647605.
1449
*
1450
* - If there are no mobile bookmarks, the query will not be created, or
1451
* will be removed if it already exists.
1452
* - If there are mobile bookmarks, the query will be created if it doesn't
1453
* exist, or will be updated with the correct title and URL otherwise.
1454
*/
1455
async ensureMobileQuery() {
1456
let db = await PlacesUtils.promiseDBConnection();
1457
1458
let mobileChildGuids = await fetchChildGuids(
1459
db,
1460
PlacesUtils.bookmarks.mobileGuid
1461
);
1462
let hasMobileBookmarks = !!mobileChildGuids.length;
1463
1464
Services.prefs.setBoolPref(MOBILE_BOOKMARKS_PREF, hasMobileBookmarks);
1465
},
1466
1467
/**
1468
* Fetches an array of GUIDs for items that have an annotation set with the
1469
* given value.
1470
*/
1471
async fetchGuidsWithAnno(anno, val) {
1472
let db = await PlacesUtils.promiseDBConnection();
1473
return fetchGuidsWithAnno(db, anno, val);
1474
},
1475
}));
1476
1477
XPCOMUtils.defineLazyGetter(this, "HistorySyncLog", () => {
1478
return Log.repository.getLogger("Sync.Engine.History.HistorySyncUtils");
1479
});
1480
1481
XPCOMUtils.defineLazyGetter(this, "BookmarkSyncLog", () => {
1482
// Use a sub-log of the bookmarks engine, so setting the level for that
1483
// engine also adjust the level of this log.
1484
return Log.repository.getLogger("Sync.Engine.Bookmarks.BookmarkSyncUtils");
1485
});
1486
1487
function validateSyncBookmarkObject(name, input, behavior) {
1488
return PlacesUtils.validateItemProperties(
1489
name,
1490
PlacesUtils.SYNC_BOOKMARK_VALIDATORS,
1491
input,
1492
behavior
1493
);
1494
}
1495
1496
// Validates a sync change record as returned by `pullChanges` and passed to
1497
// `pushChanges`.
1498
function validateChangeRecord(name, changeRecord, behavior) {
1499
return PlacesUtils.validateItemProperties(
1500
name,
1501
PlacesUtils.SYNC_CHANGE_RECORD_VALIDATORS,
1502
changeRecord,
1503
behavior
1504
);
1505
}
1506
1507
// Similar to the private `fetchBookmarksByParent` implementation in
1508
// `Bookmarks.jsm`.
1509
var fetchChildGuids = async function(db, parentGuid) {
1510
let rows = await db.executeCached(
1511
`
1512
SELECT guid
1513
FROM moz_bookmarks
1514
WHERE parent = (
1515
SELECT id FROM moz_bookmarks WHERE guid = :parentGuid
1516
)
1517
ORDER BY position`,
1518
{ parentGuid }
1519
);
1520
return rows.map(row => row.getResultByName("guid"));
1521
};
1522
1523
// A helper for whenever we want to know if a GUID doesn't exist in the places
1524
// database. Primarily used to detect orphans on incoming records.
1525
var GUIDMissing = async function(guid) {
1526
try {
1527
await PlacesUtils.promiseItemId(guid);
1528
return false;
1529
} catch (ex) {
1530
if (ex.message == "no item found for the given GUID") {
1531
return true;
1532
}
1533
throw ex;
1534
}
1535
};
1536
1537
// Legacy tag queries may use a `place:` URL that refers to the tag folder ID.
1538
// When we apply a synced tag query from a remote client, we need to update the
1539
// URL to point to the local tag.
1540
function updateTagQueryFolder(db, info) {
1541
if (
1542
info.kind != BookmarkSyncUtils.KINDS.QUERY ||
1543
!info.folder ||
1544
!info.url ||
1545
info.url.protocol != "place:"
1546
) {
1547
return info;
1548
}
1549
1550
let params = new URLSearchParams(info.url.pathname);
1551
let type = +params.get("type");
1552
if (type != Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_CONTENTS) {
1553
return info;
1554
}
1555
1556
BookmarkSyncLog.debug(
1557
`updateTagQueryFolder: Tag query folder: ${info.folder}`
1558
);
1559
1560
// Rewrite the query to directly reference the tag.
1561
params.delete("queryType");
1562
params.delete("type");
1563
params.delete("folder");
1564
params.set("tag", info.folder);
1565
info.url = new URL(info.url.protocol + params);
1566
return info;
1567
}
1568
1569
async function annotateOrphan(item, requestedParentRecordId) {
1570
let guid = BookmarkSyncUtils.recordIdToGuid(item.recordId);
1571
let itemId = await PlacesUtils.promiseItemId(guid);
1572
PlacesUtils.annotations.setItemAnnotation(
1573
itemId,
1574
BookmarkSyncUtils.SYNC_PARENT_ANNO,
1575
requestedParentRecordId,
1576
0,
1577
PlacesUtils.annotations.EXPIRE_NEVER,
1578
SOURCE_SYNC
1579
);
1580
}
1581
1582
var reparentOrphans = async function(db, item) {
1583
if (!item.kind || item.kind != BookmarkSyncUtils.KINDS.FOLDER) {
1584
return;
1585
}
1586
let orphanGuids = await fetchGuidsWithAnno(
1587
db,
1588
BookmarkSyncUtils.SYNC_PARENT_ANNO,
1589
item.recordId
1590
);
1591
let folderGuid = BookmarkSyncUtils.recordIdToGuid(item.recordId);
1592
BookmarkSyncLog.debug(
1593
`reparentOrphans: Reparenting ${JSON.stringify(orphanGuids)} to ${
1594
item.recordId
1595
}`
1596
);
1597
for (let i = 0; i < orphanGuids.length; ++i) {
1598
try {
1599
// Reparenting can fail if we have a corrupted or incomplete tree
1600
// where an item's parent is one of its descendants.
1601
BookmarkSyncLog.trace(
1602
`reparentOrphans: Attempting to move item ${
1603
orphanGuids[i]
1604
} to new parent ${item.recordId}`
1605
);
1606
await PlacesUtils.bookmarks.update({
1607
guid: orphanGuids[i],
1608
parentGuid: folderGuid,
1609
index: PlacesUtils.bookmarks.DEFAULT_INDEX,
1610
source: SOURCE_SYNC,
1611
});
1612
} catch (ex) {
1613
BookmarkSyncLog.error(
1614
`reparentOrphans: Failed to reparent item ${orphanGuids[i]} to ${
1615
item.recordId
1616
}`,
1617
ex
1618
);
1619
}
1620
}
1621
};
1622
1623
// Inserts a synced bookmark into the database.
1624
async function insertSyncBookmark(db, insertInfo) {
1625
let requestedParentRecordId = insertInfo.parentRecordId;
1626
let requestedParentGuid = BookmarkSyncUtils.recordIdToGuid(
1627
insertInfo.parentRecordId
1628
);
1629
let isOrphan = await GUIDMissing(requestedParentGuid);
1630
1631
// Default to "unfiled" for new bookmarks if the parent doesn't exist.
1632
if (!isOrphan) {
1633
BookmarkSyncLog.debug(
1634
`insertSyncBookmark: Item ${insertInfo.recordId} is not an orphan`
1635
);
1636
} else {
1637
BookmarkSyncLog.debug(
1638
`insertSyncBookmark: Item ${insertInfo.recordId} is an orphan: parent ${
1639
insertInfo.parentRecordId
1640
} doesn't exist; reparenting to unfiled`
1641
);
1642
insertInfo.parentRecordId = "unfiled";
1643
}
1644
1645
// If we're inserting a tag query, make sure the tag exists and fix the
1646
// folder ID to refer to the local tag folder.
1647
insertInfo = await updateTagQueryFolder(db, insertInfo);
1648
1649
let bookmarkInfo = syncBookmarkToPlacesBookmark(insertInfo);
1650
let bookmarkItem = await PlacesUtils.bookmarks.insert(bookmarkInfo);
1651
let newItem = await insertBookmarkMetadata(db, bookmarkItem, insertInfo);
1652
1653
// If the item is an orphan, annotate it with its real parent record ID.
1654
if (isOrphan) {
1655
await annotateOrphan(newItem, requestedParentRecordId);
1656
}
1657
1658
// Reparent all orphans that expect this folder as the parent.
1659
await reparentOrphans(db, newItem);
1660
1661
return newItem;
1662
}
1663
1664
// Keywords are a 1 to 1 mapping between strings and pairs of (URL, postData).
1665
// (the postData is not synced, so we ignore it). Sync associates keywords with
1666
// bookmarks, which is not really accurate. -- We might already have a keyword
1667
// with that name, or we might already have another bookmark with that URL with
1668
// a different keyword, etc.
1669
//
1670
// If we don't handle those cases by removing the conflicting keywords first,
1671
// the insertion will fail, and the keywords will either be wrong, or missing.
1672
// This function handles those cases.
1673
function removeConflictingKeywords(bookmarkURL, newKeyword) {
1674
return PlacesUtils.withConnectionWrapper(
1675
"BookmarkSyncUtils: removeConflictingKeywords",
1676
async function(db) {
1677
let entryForURL = await PlacesUtils.keywords.fetch({
1678
url: bookmarkURL.href,
1679
});
1680
if (entryForURL && entryForURL.keyword !== newKeyword) {
1681
await PlacesUtils.keywords.remove({
1682
keyword: entryForURL.keyword,
1683
source: SOURCE_SYNC,
1684
});
1685
// This will cause us to reupload this record for this sync, but
1686
// without it, we will risk data corruption.
1687
await BookmarkSyncUtils.addSyncChangesForBookmarksWithURL(
1688
db,
1689
entryForURL.url,
1690
1
1691
);
1692
}
1693
if (!newKeyword) {
1694
return;
1695
}
1696
let entryForNewKeyword = await PlacesUtils.keywords.fetch({
1697
keyword: newKeyword,
1698
});
1699
if (entryForNewKeyword) {
1700
await PlacesUtils.keywords.remove({
1701
keyword: entryForNewKeyword.keyword,
1702
source: SOURCE_SYNC,
1703
});
1704
await BookmarkSyncUtils.addSyncChangesForBookmarksWithURL(
1705
db,
1706
entryForNewKeyword.url,
1707
1
1708
);
1709
}
1710
}
1711
);
1712
}
1713
1714
// Sets annotations, keywords, and tags on a new bookmark. Returns a Sync
1715
// bookmark object.
1716
async function insertBookmarkMetadata(db, bookmarkItem, insertInfo) {
1717
let newItem = await placesBookmarkToSyncBookmark(db, bookmarkItem);
1718
1719
try {
1720
newItem.tags = tagItem(bookmarkItem, insertInfo.tags);
1721
} catch (ex) {
1722
BookmarkSyncLog.warn(
1723
`insertBookmarkMetadata: Error tagging item ${insertInfo.recordId}`,
1724
ex
1725
);
1726
}
1727
1728
if (insertInfo.keyword) {
1729
await removeConflictingKeywords(bookmarkItem.url, insertInfo.keyword);
1730
await PlacesUtils.keywords.insert({
1731
keyword: insertInfo.keyword,
1732
url: bookmarkItem.url.href,
1733
source: SOURCE_SYNC,
1734
});
1735
newItem.keyword = insertInfo.keyword;
1736
}
1737
1738
return newItem;
1739
}
1740
1741
// Determines the Sync record kind for an existing bookmark.
1742
async function getKindForItem(db, item) {
1743
switch (item.type) {
1744
case PlacesUtils.bookmarks.TYPE_FOLDER: {
1745
return BookmarkSyncUtils.KINDS.FOLDER;
1746
}
1747
case PlacesUtils.bookmarks.TYPE_BOOKMARK:
1748
return item.url.protocol == "place:"
1749
? BookmarkSyncUtils.KINDS.QUERY
1750
: BookmarkSyncUtils.KINDS.BOOKMARK;
1751
1752
case PlacesUtils.bookmarks.TYPE_SEPARATOR:
1753
return BookmarkSyncUtils.KINDS.SEPARATOR;
1754
}
1755
return null;
1756
}
1757
1758
// Returns the `nsINavBookmarksService` bookmark type constant for a Sync
1759
// record kind.
1760
function getTypeForKind(kind) {
1761
switch (kind) {
1762
case BookmarkSyncUtils.KINDS.BOOKMARK:
1763
case BookmarkSyncUtils.KINDS.QUERY:
1764
return PlacesUtils.bookmarks.TYPE_BOOKMARK;
1765
1766
case BookmarkSyncUtils.KINDS.FOLDER:
1767
return PlacesUtils.bookmarks.TYPE_FOLDER;
1768
1769
case BookmarkSyncUtils.KINDS.SEPARATOR:
1770
return PlacesUtils.bookmarks.TYPE_SEPARATOR;
1771
}
1772
throw new Error(`Unknown bookmark kind: ${kind}`);
1773
}
1774
1775
async function updateSyncBookmark(db, updateInfo) {
1776
let guid = BookmarkSyncUtils.recordIdToGuid(updateInfo.recordId);
1777
let oldBookmarkItem = await PlacesUtils.bookmarks.fetch(guid);
1778
if (!oldBookmarkItem) {
1779
throw new Error(
1780
`Bookmark with record ID ${updateInfo.recordId} does not exist`
1781
);
1782
}
1783
1784
if (updateInfo.hasOwnProperty("dateAdded")) {
1785
let newDateAdded = BookmarkSyncUtils.ratchetTimestampBackwards(
1786
oldBookmarkItem.dateAdded,
1787
updateInfo.dateAdded
1788
);
1789
if (!newDateAdded || newDateAdded === oldBookmarkItem.dateAdded) {
1790
delete updateInfo.dateAdded;
1791
} else {
1792
updateInfo.dateAdded = newDateAdded;
1793
}
1794
}
1795
1796
let shouldReinsert = false;
1797
let oldKind = await getKindForItem(db, oldBookmarkItem);
1798
if (updateInfo.hasOwnProperty("kind") && updateInfo.kind != oldKind) {
1799
// If the item's aren't the same kind, we can't update the record;
1800
// we must remove and reinsert.
1801
shouldReinsert = true;
1802
if (BookmarkSyncLog.level <= Log.Level.Warn) {
1803
let oldRecordId = BookmarkSyncUtils.guidToRecordId(oldBookmarkItem.guid);
1804
BookmarkSyncLog.warn(
1805
`updateSyncBookmark: Local ${oldRecordId} kind = ${oldKind}; remote ${
1806
updateInfo.recordId
1807
} kind = ${updateInfo.kind}. Deleting and recreating`
1808
);
1809
}
1810
}
1811
1812
if (shouldReinsert) {
1813
if (!updateInfo.hasOwnProperty("dateAdded")) {
1814
updateInfo.dateAdded = oldBookmarkItem.dateAdded.getTime();
1815
}
1816
let newInfo = validateNewBookmark(
1817
"BookmarkSyncUtils: reinsert",
1818
updateInfo
1819
);
1820
await PlacesUtils.bookmarks.remove({
1821
guid,
1822
source: SOURCE_SYNC,
1823
});
1824
// A reinsertion likely indicates a confused client, since there aren't
1825
// public APIs for changing an item's kind (e.g., turning
1826
// a folder into a separator while preserving its annos and position).
1827
// This might be a good case to repair later; for now, we assume Sync has
1828
// passed a complete record for the new item, and don't try to merge
1829
// `oldBookmarkItem` with `updateInfo`.
1830
return insertSyncBookmark(db, newInfo);
1831
}
1832
1833
let isOrphan = false,
1834
requestedParentRecordId;
1835
if (updateInfo.hasOwnProperty("parentRecordId")) {
1836
requestedParentRecordId = updateInfo.parentRecordId;
1837
let oldParentRecordId = BookmarkSyncUtils.guidToRecordId(
1838
oldBookmarkItem.parentGuid
1839
);
1840
if (requestedParentRecordId != oldParentRecordId) {
1841
if (PlacesUtils.isRootItem(oldBookmarkItem.guid)) {
1842
throw new Error(`Cannot move Places root ${oldBookmarkItem.guid}`);
1843
}
1844
let requestedParentGuid = BookmarkSyncUtils.recordIdToGuid(
1845
requestedParentRecordId
1846
);
1847
isOrphan = await GUIDMissing(requestedParentGuid);
1848
if (!isOrphan) {
1849
BookmarkSyncLog.debug(
1850
`updateSyncBookmark: Item ${updateInfo.recordId} is not an orphan`
1851
);
1852
} else {
1853
// Don't move the item if the new parent doesn't exist. Instead, mark
1854
// the item as an orphan. We'll annotate it with its real parent after
1855
// updating.
1856
BookmarkSyncLog.trace(
1857
`updateSyncBookmark: Item ${
1858
updateInfo.recordId
1859
} is an orphan: could not find parent ${requestedParentRecordId}`
1860
);
1861
delete updateInfo.parentRecordId;
1862
}
1863
} else {
1864
// If the parent is the same, just omit it so that `update` doesn't do
1865
// extra work.
1866
delete updateInfo.parentRecordId;
1867
}
1868
}
1869
1870
updateInfo = await updateTagQueryFolder(db, updateInfo);
1871
1872
let bookmarkInfo = syncBookmarkToPlacesBookmark(updateInfo);
1873
let newBookmarkItem = shouldUpdateBookmark(bookmarkInfo)
1874
? await PlacesUtils.bookmarks.update(bookmarkInfo)
1875
: oldBookmarkItem;
1876
let newItem = await updateBookmarkMetadata(
1877
db,
1878
oldBookmarkItem,
1879
newBookmarkItem,
1880
updateInfo
1881
);
1882
1883
// If the item is an orphan, annotate it with its real parent record ID.
1884
if (isOrphan) {
1885
await annotateOrphan(newItem, requestedParentRecordId);
1886
}
1887
1888
// Reparent all orphans that expect this folder as the parent.
1889
await reparentOrphans(db, newItem);
1890
1891
return newItem;
1892
}
1893
1894
// Updates tags, keywords, and annotations for an existing bookmark. Returns a
1895
// Sync bookmark object.
1896
async function updateBookmarkMetadata(
1897
db,
1898
oldBookmarkItem,
1899
newBookmarkItem,
1900
updateInfo
1901
) {
1902
let newItem = await placesBookmarkToSyncBookmark(db, newBookmarkItem);
1903
1904
try {
1905
newItem.tags = tagItem(newBookmarkItem, updateInfo.tags);
1906
} catch (ex) {
1907
BookmarkSyncLog.warn(
1908
`updateBookmarkMetadata: Error tagging item ${updateInfo.recordId}`,
1909
ex
1910
);
1911
}
1912
1913
if (updateInfo.hasOwnProperty("keyword")) {
1914
// Unconditionally remove the old keyword.
1915
await removeConflictingKeywords(oldBookmarkItem.url, updateInfo.keyword);
1916
if (updateInfo.keyword) {
1917
await PlacesUtils.keywords.insert({
1918
keyword: updateInfo.keyword,
1919
url: newItem.url.href,
1920
source: SOURCE_SYNC,
1921
});
1922
}
1923
newItem.keyword = updateInfo.keyword;
1924
}
1925
1926
return newItem;
1927
}
1928
1929
function validateNewBookmark(name, info) {
1930
let insertInfo = validateSyncBookmarkObject(name, info, {
1931
kind: { required: true },
1932
recordId: { required: true },
1933
url: {
1934
requiredIf: b =>
1935
[
1936
BookmarkSyncUtils.KINDS.BOOKMARK,
1937
BookmarkSyncUtils.KINDS.QUERY,
1938
].includes(b.kind),
1939
validIf: b =>
1940
[
1941
BookmarkSyncUtils.KINDS.BOOKMARK,
1942
BookmarkSyncUtils.KINDS.QUERY,
1943
].includes(b.kind),
1944
},
1945
parentRecordId: { required: true },
1946
title: {
1947
validIf: b =>
1948
[
1949
BookmarkSyncUtils.KINDS.BOOKMARK,
1950
BookmarkSyncUtils.KINDS.QUERY,
1951
BookmarkSyncUtils.KINDS.FOLDER,
1952
].includes(b.kind) || b.title === "",
1953
},
1954
query: { validIf: b => b.kind == BookmarkSyncUtils.KINDS.QUERY },
1955
folder: { validIf: b => b.kind == BookmarkSyncUtils.KINDS.QUERY },
1956
tags: {
1957
validIf: b =>
1958
[
1959
BookmarkSyncUtils.KINDS.BOOKMARK,
1960
BookmarkSyncUtils.KINDS.QUERY,
1961
].includes(b.kind),
1962
},
1963
keyword: {
1964
validIf: b =>
1965
[
1966
BookmarkSyncUtils.KINDS.BOOKMARK,
1967
BookmarkSyncUtils.KINDS.QUERY,
1968
].includes(b.kind),
1969
},
1970
dateAdded: { required: false },
1971
});
1972
1973
return insertInfo;
1974
}
1975
1976
async function fetchGuidsWithAnno(db, anno, val) {
1977
let rows = await db.executeCached(
1978
`
1979
SELECT b.guid FROM moz_items_annos a
1980
JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id
1981
JOIN moz_bookmarks b ON b.id = a.item_id
1982
WHERE n.name = :anno AND
1983
a.content = :val`,
1984
{ anno, val }
1985
);
1986
return rows.map(row => row.getResultByName("guid"));
1987
}
1988
1989
function tagItem(item, tags) {
1990
if (!item.url) {
1991
return [];
1992
}
1993
1994
// Remove leading and trailing whitespace, then filter out empty tags.
1995
let newTags = tags ? tags.map(tag => tag.trim()).filter(Boolean) : [];
1996
1997
// Removing the last tagged item will also remove the tag. To preserve
1998
// tag IDs, we temporarily tag a dummy URI, ensuring the tags exist.
1999
let dummyURI = PlacesUtils.toURI("about:weave#BStore_tagURI");
2000
let bookmarkURI = PlacesUtils.toURI(item.url.href);
2001
if (newTags && newTags.length) {
2002
PlacesUtils.tagging.tagURI(dummyURI, newTags, SOURCE_SYNC);
2003
}
2004
PlacesUtils.tagging.untagURI(bookmarkURI, null, SOURCE_SYNC);
2005
if (newTags && newTags.length) {
2006
PlacesUtils.tagging.tagURI(bookmarkURI, newTags, SOURCE_SYNC);
2007
}
2008
PlacesUtils.tagging.untagURI(dummyURI, null, SOURCE_SYNC);
2009
2010
return newTags;
2011
}
2012
2013
// `PlacesUtils.bookmarks.update` checks if we've supplied enough properties,
2014
// but doesn't know about additional livemark properties. We check this to avoid
2015
// having it throw in case we only pass properties like `{ guid, feedURI }`.
2016
function shouldUpdateBookmark(bookmarkInfo) {
2017
return (
2018
bookmarkInfo.hasOwnProperty("parentGuid") ||
2019
bookmarkInfo.hasOwnProperty("title") ||
2020
bookmarkInfo.hasOwnProperty("url")
2021
);
2022
}
2023
2024
// Converts a Places bookmark to a Sync bookmark. This function maps Places
2025
// GUIDs to record IDs and filters out extra Places properties like date added,
2026
// last modified, and index.
2027
async function placesBookmarkToSyncBookmark(db, bookmarkItem) {
2028
let item = {};
2029
2030
for (let prop in bookmarkItem) {
2031
switch (prop) {
2032
// Record IDs are identical to Places GUIDs for all items except roots.
2033
case "guid":
2034
item.recordId = BookmarkSyncUtils.guidToRecordId(bookmarkItem.guid);
2035
break;
2036
2037
case "parentGuid":
2038
item.parentRecordId = BookmarkSyncUtils.guidToRecordId(
2039
bookmarkItem.parentGuid
2040
);
2041
break;
2042
2043
// Sync uses kinds instead of types, which distinguish between folders,
2044
// livemarks, bookmarks, and queries.
2045
case "type":
2046
item.kind = await getKindForItem(db, bookmarkItem);
2047
break;
2048
2049
case "title":
2050
case "url":
2051
item[prop] = bookmarkItem[prop];
2052
break;
2053
2054
case "dateAdded":
2055
item[prop] = new Date(bookmarkItem[prop]).getTime();
2056
break;
2057
}
2058
}
2059
2060
return item;
2061
}
2062
2063
// Converts a Sync bookmark object to a Places bookmark or livemark object.
2064
// This function maps record IDs to Places GUIDs, and filters out extra Sync
2065
// properties like keywords, tags. Returns an object that can be passed to
2066
// `PlacesUtils.bookmarks.{insert, update}`.
2067
function syncBookmarkToPlacesBookmark(info) {
2068
let bookmarkInfo = {
2069
source: SOURCE_SYNC,
2070
};
2071
2072
for (let prop in info) {
2073
switch (prop) {
2074
case "kind":
2075
bookmarkInfo.type = getTypeForKind(info.kind);
2076
break;
2077
2078
// Convert record IDs to Places GUIDs for roots.
2079
case "recordId":
2080
bookmarkInfo.guid = BookmarkSyncUtils.recordIdToGuid(info.recordId);
2081
break;
2082
2083
case "dateAdded":
2084
bookmarkInfo.dateAdded = new Date(info.dateAdded);
2085
break;
2086
2087
case "parentRecordId":
2088
bookmarkInfo.parentGuid = BookmarkSyncUtils.recordIdToGuid(
2089