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 file,
3
* You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5
"use strict";
6
7
/**
8
* This module provides an asynchronous API for managing bookmarks.
9
*
10
* Bookmarks are organized in a tree structure, and include URLs, folders and
11
* separators. Multiple bookmarks for the same URL are allowed.
12
*
13
* Note that if you are handling bookmarks operations in the UI, you should
14
* not use this API directly, but rather use PlacesTransactions.jsm, so that
15
* any operation is undo/redo-able.
16
*
17
* Each bookmark-item is represented by an object having the following
18
* properties:
19
*
20
* - guid (string)
21
* The globally unique identifier of the item.
22
* - parentGuid (string)
23
* The globally unique identifier of the folder containing the item.
24
* This will be an empty string for the Places root folder.
25
* - index (number)
26
* The 0-based position of the item in the parent folder.
27
* - dateAdded (Date)
28
* The time at which the item was added.
29
* - lastModified (Date)
30
* The time at which the item was last modified.
31
* - type (number)
32
* The item's type, either TYPE_BOOKMARK, TYPE_FOLDER or TYPE_SEPARATOR.
33
*
34
* The following properties are only valid for URLs or folders.
35
*
36
* - title (string)
37
* The item's title, if any. Empty titles and null titles are considered
38
* the same. Titles longer than DB_TITLE_LENGTH_MAX will be truncated.
39
*
40
* The following properties are only valid for URLs:
41
*
42
* - url (URL, href or nsIURI)
43
* The item's URL. Note that while input objects can contains either
44
* an URL object, an href string, or an nsIURI, output objects will always
45
* contain an URL object.
46
* An URL cannot be longer than DB_URL_LENGTH_MAX, methods will throw if a
47
* longer value is provided.
48
*
49
* Each successful operation notifies through the nsINavBookmarksObserver
50
* interface. To listen to such notifications you must register using
51
* nsINavBookmarksService addObserver and removeObserver methods.
52
* Note that bookmark addition or order changes won't notify onItemMoved for
53
* items that have their indexes changed.
54
* Similarly, lastModified changes not done explicitly (like changing another
55
* property) won't fire an onItemChanged notification for the lastModified
56
* property.
57
* @see nsINavBookmarkObserver
58
*/
59
60
var EXPORTED_SYMBOLS = ["Bookmarks"];
61
62
const { XPCOMUtils } = ChromeUtils.import(
64
);
65
66
XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
67
68
ChromeUtils.defineModuleGetter(
69
this,
70
"NetUtil",
72
);
73
ChromeUtils.defineModuleGetter(
74
this,
75
"PlacesUtils",
77
);
78
ChromeUtils.defineModuleGetter(
79
this,
80
"PlacesSyncUtils",
82
);
83
84
// This is an helper to temporarily cover the need to know the tags folder
85
// itemId until bug 424160 is fixed. This exists so that startup paths won't
86
// pay the price to initialize the bookmarks service just to fetch this value.
87
// If the method is already initing the bookmarks service for other reasons
88
// (most of the writing methods will invoke getObservers() already) it can
89
// directly use the PlacesUtils.tagsFolderId property.
90
var gTagsFolderId;
91
async function promiseTagsFolderId() {
92
if (gTagsFolderId) {
93
return gTagsFolderId;
94
}
95
let db = await PlacesUtils.promiseDBConnection();
96
let rows = await db.execute(
97
"SELECT id FROM moz_bookmarks WHERE guid = :guid",
98
{ guid: Bookmarks.tagsGuid }
99
);
100
return (gTagsFolderId = rows[0].getResultByName("id"));
101
}
102
103
const MATCH_ANYWHERE_UNMODIFIED =
104
Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE_UNMODIFIED;
105
const BEHAVIOR_BOOKMARK = Ci.mozIPlacesAutoComplete.BEHAVIOR_BOOKMARK;
106
107
var Bookmarks = Object.freeze({
108
/**
109
* Item's type constants.
110
* These should stay consistent with nsINavBookmarksService.idl
111
*/
112
TYPE_BOOKMARK: 1,
113
TYPE_FOLDER: 2,
114
TYPE_SEPARATOR: 3,
115
116
/**
117
* Sync status constants, stored for each item.
118
*/
119
SYNC_STATUS: {
120
UNKNOWN: Ci.nsINavBookmarksService.SYNC_STATUS_UNKNOWN,
121
NEW: Ci.nsINavBookmarksService.SYNC_STATUS_NEW,
122
NORMAL: Ci.nsINavBookmarksService.SYNC_STATUS_NORMAL,
123
},
124
125
/**
126
* Default index used to append a bookmark-item at the end of a folder.
127
* This should stay consistent with nsINavBookmarksService.idl
128
*/
129
DEFAULT_INDEX: -1,
130
131
/**
132
* Maximum length of a tag.
133
* Any tag above this length is rejected.
134
*/
135
MAX_TAG_LENGTH: 100,
136
137
/**
138
* Bookmark change source constants, passed as optional properties and
139
* forwarded to observers. See nsINavBookmarksService.idl for an explanation.
140
*/
141
SOURCES: {
142
DEFAULT: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
143
SYNC: Ci.nsINavBookmarksService.SOURCE_SYNC,
144
IMPORT: Ci.nsINavBookmarksService.SOURCE_IMPORT,
145
SYNC_REPARENT_REMOVED_FOLDER_CHILDREN:
146
Ci.nsINavBookmarksService.SOURCE_SYNC_REPARENT_REMOVED_FOLDER_CHILDREN,
147
RESTORE: Ci.nsINavBookmarksService.SOURCE_RESTORE,
148
RESTORE_ON_STARTUP: Ci.nsINavBookmarksService.SOURCE_RESTORE_ON_STARTUP,
149
},
150
151
/**
152
* Special GUIDs associated with bookmark roots.
153
* It's guaranteed that the roots will always have these guids.
154
*/
155
rootGuid: "root________",
156
menuGuid: "menu________",
157
toolbarGuid: "toolbar_____",
158
unfiledGuid: "unfiled_____",
159
mobileGuid: "mobile______",
160
161
// With bug 424160, tags will stop being bookmarks, thus this root will
162
// be removed. Do not rely on this, rather use the tagging service API.
163
tagsGuid: "tags________",
164
165
/**
166
* The GUIDs of the user content root folders that we support, for easy access
167
* as a set.
168
*/
169
userContentRoots: [
170
"toolbar_____",
171
"menu________",
172
"unfiled_____",
173
"mobile______",
174
],
175
176
/**
177
* GUIDs associated with virtual queries that are used for displaying bookmark
178
* folders in the left pane.
179
*/
180
virtualMenuGuid: "menu_______v",
181
virtualToolbarGuid: "toolbar____v",
182
virtualUnfiledGuid: "unfiled____v",
183
virtualMobileGuid: "mobile_____v",
184
185
/**
186
* Checks if a guid is a virtual root.
187
*
188
* @param {String} guid The guid of the item to look for.
189
* @returns {Boolean} true if guid is a virtual root, false otherwise.
190
*/
191
isVirtualRootItem(guid) {
192
return (
193
guid == PlacesUtils.bookmarks.virtualMenuGuid ||
194
guid == PlacesUtils.bookmarks.virtualToolbarGuid ||
195
guid == PlacesUtils.bookmarks.virtualUnfiledGuid ||
196
guid == PlacesUtils.bookmarks.virtualMobileGuid
197
);
198
},
199
200
/**
201
* Returns the title to use on the UI for a bookmark item. Root folders
202
* in the database don't store fully localised versions of the title. To
203
* get those this function should be called.
204
*
205
* Hence, this function should only be called if a root folder object is
206
* likely to be displayed to the user.
207
*
208
* @param {Object} info An object representing a bookmark-item.
209
* @returns {String} The correct string.
210
* @throws {Error} If the guid in PlacesUtils.bookmarks.userContentRoots is
211
* not supported.
212
*/
213
getLocalizedTitle(info) {
214
if (!PlacesUtils.bookmarks.userContentRoots.includes(info.guid)) {
215
return info.title;
216
}
217
218
switch (info.guid) {
219
case PlacesUtils.bookmarks.toolbarGuid:
220
return PlacesUtils.getString("BookmarksToolbarFolderTitle");
221
case PlacesUtils.bookmarks.menuGuid:
222
return PlacesUtils.getString("BookmarksMenuFolderTitle");
223
case PlacesUtils.bookmarks.unfiledGuid:
224
return PlacesUtils.getString("OtherBookmarksFolderTitle");
225
case PlacesUtils.bookmarks.mobileGuid:
226
return PlacesUtils.getString("MobileBookmarksFolderTitle");
227
default:
228
throw new Error(
229
`Unsupported guid ${info.guid} passed to getLocalizedTitle!`
230
);
231
}
232
},
233
234
/**
235
* Inserts a bookmark-item into the bookmarks tree.
236
*
237
* For creating a bookmark, the following set of properties is required:
238
* - type
239
* - parentGuid
240
* - url, only for bookmarked URLs
241
*
242
* If an index is not specified, it defaults to appending.
243
* It's also possible to pass a non-existent GUID to force creation of an
244
* item with the given GUID, but unless you have a very sound reason, such as
245
* an undo manager implementation or synchronization, don't do that.
246
*
247
* Note that any known properties that don't apply to the specific item type
248
* cause an exception.
249
*
250
* @param info
251
* object representing a bookmark-item.
252
*
253
* @return {Promise} resolved when the creation is complete.
254
* @resolves to an object representing the created bookmark.
255
* @rejects if it's not possible to create the requested bookmark.
256
* @throws if the arguments are invalid.
257
*/
258
insert(info) {
259
let now = new Date();
260
let addedTime = (info && info.dateAdded) || now;
261
let modTime = addedTime;
262
if (addedTime > now) {
263
modTime = now;
264
}
265
let insertInfo = validateBookmarkObject("Bookmarks.jsm: insert", info, {
266
type: { defaultValue: this.TYPE_BOOKMARK },
267
index: { defaultValue: this.DEFAULT_INDEX },
268
url: {
269
requiredIf: b => b.type == this.TYPE_BOOKMARK,
270
validIf: b => b.type == this.TYPE_BOOKMARK,
271
},
272
parentGuid: {
273
required: true,
274
// Inserting into the root folder is not allowed.
275
validIf: b => b.parentGuid != this.rootGuid,
276
},
277
title: {
278
defaultValue: "",
279
validIf: b =>
280
b.type == this.TYPE_BOOKMARK ||
281
b.type == this.TYPE_FOLDER ||
282
b.title === "",
283
},
284
dateAdded: { defaultValue: addedTime },
285
lastModified: {
286
defaultValue: modTime,
287
validIf: b =>
288
b.lastModified >= now ||
289
(b.dateAdded && b.lastModified >= b.dateAdded),
290
},
291
source: { defaultValue: this.SOURCES.DEFAULT },
292
});
293
294
return (async () => {
295
// Ensure the parent exists.
296
let parent = await fetchBookmark({ guid: insertInfo.parentGuid });
297
if (!parent) {
298
throw new Error("parentGuid must be valid");
299
}
300
301
// Set index in the appending case.
302
if (
303
insertInfo.index == this.DEFAULT_INDEX ||
304
insertInfo.index > parent._childCount
305
) {
306
insertInfo.index = parent._childCount;
307
}
308
309
let item = await insertBookmark(insertInfo, parent);
310
311
// We need the itemId to notify, though once the switch to guids is
312
// complete we may stop using it.
313
let itemId = await PlacesUtils.promiseItemId(item.guid);
314
315
// Pass tagging information for the observers to skip over these notifications when needed.
316
let isTagging = parent._parentId == PlacesUtils.tagsFolderId;
317
let isTagsFolder = parent._id == PlacesUtils.tagsFolderId;
318
let url = "";
319
if (item.type == Bookmarks.TYPE_BOOKMARK) {
320
url = item.url.href;
321
}
322
323
let notification = new PlacesBookmarkAddition({
324
id: itemId,
325
url,
326
itemType: item.type,
327
parentId: parent._id,
328
index: item.index,
329
title: item.title,
330
dateAdded: item.dateAdded,
331
guid: item.guid,
332
parentGuid: item.parentGuid,
333
source: item.source,
334
isTagging: isTagging || isTagsFolder,
335
});
336
PlacesObservers.notifyListeners([notification]);
337
338
// If it's a tag, notify OnItemChanged to all bookmarks for this URL.
339
if (isTagging) {
340
let observers = PlacesUtils.bookmarks.getObservers();
341
for (let entry of await fetchBookmarksByURL(item, {
342
concurrent: true,
343
})) {
344
notify(observers, "onItemChanged", [
345
entry._id,
346
"tags",
347
false,
348
"",
349
PlacesUtils.toPRTime(entry.lastModified),
350
entry.type,
351
entry._parentId,
352
entry.guid,
353
entry.parentGuid,
354
"",
355
item.source,
356
]);
357
}
358
}
359
360
// Remove non-enumerable properties.
361
delete item.source;
362
return Object.assign({}, item);
363
})();
364
},
365
366
/**
367
* Inserts a bookmark-tree into the existing bookmarks tree.
368
*
369
* All the specified folders and bookmarks will be inserted as new, even
370
* if duplicates. There's no merge support at this time.
371
*
372
* The input should be of the form:
373
* {
374
* guid: "<some-existing-guid-to-use-as-parent>",
375
* source: "<some valid source>", (optional)
376
* children: [
377
* ... valid bookmark objects.
378
* ]
379
* }
380
*
381
* Children will be appended to any existing children of the parent
382
* that is specified. The source specified on the root of the tree
383
* will be used for all the items inserted. Any indices or custom parentGuids
384
* set on children will be ignored and overwritten.
385
*
386
* @param {Object} tree
387
* object representing a tree of bookmark items to insert.
388
* @param {Object} options [optional]
389
* object with properties representing options. Current options are:
390
* - fixupOrSkipInvalidEntries: makes the insert more lenient to
391
* mistakes in the input tree. Properties of an entry that are
392
* fixable will be corrected, otherwise the entry will be skipped.
393
* This is particularly convenient for import/restore operations,
394
* but should not be abused for common inserts, since it may hide
395
* bugs in the calling code.
396
*
397
* @return {Promise} resolved when the creation is complete.
398
* @resolves to an array of objects representing the created bookmark(s).
399
* @rejects if it's not possible to create the requested bookmark.
400
* @throws if the arguments are invalid.
401
*/
402
insertTree(tree, options) {
403
if (!tree || typeof tree != "object") {
404
throw new Error("Should be provided a valid tree object.");
405
}
406
if (!Array.isArray(tree.children) || !tree.children.length) {
407
throw new Error("Should have a non-zero number of children to insert.");
408
}
409
if (!PlacesUtils.isValidGuid(tree.guid)) {
410
throw new Error(
411
`The parent guid is not valid (${tree.guid} ${tree.title}).`
412
);
413
}
414
if (tree.guid == this.rootGuid) {
415
throw new Error("Can't insert into the root.");
416
}
417
if (tree.guid == this.tagsGuid) {
418
throw new Error("Can't use insertTree to insert tags.");
419
}
420
if (
421
tree.hasOwnProperty("source") &&
422
!Object.values(this.SOURCES).includes(tree.source)
423
) {
424
throw new Error("Can't use source value " + tree.source);
425
}
426
if (options && typeof options != "object") {
427
throw new Error("Options should be a valid object");
428
}
429
let fixupOrSkipInvalidEntries =
430
options && !!options.fixupOrSkipInvalidEntries;
431
432
// Serialize the tree into an array of items to insert into the db.
433
let insertInfos = [];
434
let urlsThatMightNeedPlaces = [];
435
436
// We want to use the same 'last added' time for all the entries
437
// we import (so they won't differ by a few ms based on where
438
// they are in the tree, and so we don't needlessly construct
439
// multiple dates).
440
let fallbackLastAdded = new Date();
441
442
const { TYPE_BOOKMARK, TYPE_FOLDER, SOURCES } = this;
443
444
// Reuse the 'source' property for all the entries.
445
let source = tree.source || SOURCES.DEFAULT;
446
447
// This is recursive.
448
function appendInsertionInfoForInfoArray(infos, indexToUse, parentGuid) {
449
// We want to keep the index of items that will be inserted into the root
450
// NULL, and then use a subquery to select the right index, to avoid
451
// races where other consumers might add items between when we determine
452
// the index and when we insert. However, the validator does not allow
453
// NULL values in in the index, so we fake it while validating and then
454
// correct later. Keep track of whether we're doing this:
455
let shouldUseNullIndices = false;
456
if (indexToUse === null) {
457
shouldUseNullIndices = true;
458
indexToUse = 0;
459
}
460
461
// When a folder gets an item added, its last modified date is updated
462
// to be equal to the date we added the item (if that date is newer).
463
// Because we're inserting a tree, we keep track of this date for the
464
// loop, updating it for inserted items as well as from any subfolders
465
// we insert.
466
let lastAddedForParent = new Date(0);
467
for (let info of infos) {
468
// Ensure to use the same date for dateAdded and lastModified, even if
469
// dateAdded may be imposed by the caller.
470
let time = (info && info.dateAdded) || fallbackLastAdded;
471
let insertInfo = {
472
guid: { defaultValue: PlacesUtils.history.makeGuid() },
473
type: { defaultValue: TYPE_BOOKMARK },
474
url: {
475
requiredIf: b => b.type == TYPE_BOOKMARK,
476
validIf: b => b.type == TYPE_BOOKMARK,
477
},
478
parentGuid: { replaceWith: parentGuid }, // Set the correct parent guid.
479
title: {
480
defaultValue: "",
481
validIf: b =>
482
b.type == TYPE_BOOKMARK ||
483
b.type == TYPE_FOLDER ||
484
b.title === "",
485
},
486
dateAdded: {
487
defaultValue: time,
488
validIf: b => !b.lastModified || b.dateAdded <= b.lastModified,
489
},
490
lastModified: {
491
defaultValue: time,
492
validIf: b =>
493
(!b.dateAdded && b.lastModified >= time) ||
494
(b.dateAdded && b.lastModified >= b.dateAdded),
495
},
496
index: { replaceWith: indexToUse++ },
497
source: { replaceWith: source },
498
keyword: { validIf: b => b.type == TYPE_BOOKMARK },
499
charset: { validIf: b => b.type == TYPE_BOOKMARK },
500
postData: { validIf: b => b.type == TYPE_BOOKMARK },
501
tags: { validIf: b => b.type == TYPE_BOOKMARK },
502
children: {
503
validIf: b => b.type == TYPE_FOLDER && Array.isArray(b.children),
504
},
505
};
506
if (fixupOrSkipInvalidEntries) {
507
insertInfo.guid.fixup = b =>
508
(b.guid = PlacesUtils.history.makeGuid());
509
insertInfo.dateAdded.fixup = insertInfo.lastModified.fixup = b =>
510
(b.lastModified = b.dateAdded = fallbackLastAdded);
511
}
512
try {
513
insertInfo = validateBookmarkObject(
514
"Bookmarks.jsm: insertTree",
515
info,
516
insertInfo
517
);
518
} catch (ex) {
519
if (fixupOrSkipInvalidEntries) {
520
indexToUse--;
521
continue;
522
} else {
523
throw ex;
524
}
525
}
526
527
if (shouldUseNullIndices) {
528
insertInfo.index = null;
529
}
530
// Store the URL if this is a bookmark, so we can ensure we create an
531
// entry in moz_places for it.
532
if (insertInfo.type == Bookmarks.TYPE_BOOKMARK) {
533
urlsThatMightNeedPlaces.push(insertInfo.url);
534
}
535
536
insertInfos.push(insertInfo);
537
// Process any children. We have to use info.children here rather than
538
// insertInfo.children because validateBookmarkObject doesn't copy over
539
// the children ref, as the default bookmark validators object doesn't
540
// know about children.
541
if (info.children) {
542
// start children of this item off at index 0.
543
let childrenLastAdded = appendInsertionInfoForInfoArray(
544
info.children,
545
0,
546
insertInfo.guid
547
);
548
if (childrenLastAdded > insertInfo.lastModified) {
549
insertInfo.lastModified = childrenLastAdded;
550
}
551
if (childrenLastAdded > lastAddedForParent) {
552
lastAddedForParent = childrenLastAdded;
553
}
554
}
555
556
// Ensure we track what time to update the parent to.
557
if (insertInfo.dateAdded > lastAddedForParent) {
558
lastAddedForParent = insertInfo.dateAdded;
559
}
560
}
561
return lastAddedForParent;
562
}
563
564
// We want to validate synchronously, but we can't know the index at which
565
// we're inserting into the parent. We just use NULL instead,
566
// and the SQL query with which we insert will update it as necessary.
567
let lastAddedForParent = appendInsertionInfoForInfoArray(
568
tree.children,
569
null,
570
tree.guid
571
);
572
573
// appendInsertionInfoForInfoArray will remove invalid items and may leave
574
// us with nothing to insert, if so, just return early.
575
if (!insertInfos.length) {
576
return [];
577
}
578
579
return (async function() {
580
let treeParent = await fetchBookmark({ guid: tree.guid });
581
if (!treeParent) {
582
throw new Error("The parent you specified doesn't exist.");
583
}
584
585
if (treeParent._parentId == PlacesUtils.tagsFolderId) {
586
throw new Error("Can't use insertTree to insert tags.");
587
}
588
589
await insertBookmarkTree(
590
insertInfos,
591
source,
592
treeParent,
593
urlsThatMightNeedPlaces,
594
lastAddedForParent
595
);
596
597
// Now update the indices of root items in the objects we return.
598
// These may be wrong if someone else modified the table between
599
// when we fetched the parent and inserted our items, but the actual
600
// inserts will have been correct, and we don't want to query the DB
601
// again if we don't have to. bug 1347230 covers improving this.
602
let rootIndex = treeParent._childCount;
603
for (let insertInfo of insertInfos) {
604
if (insertInfo.parentGuid == tree.guid) {
605
insertInfo.index += rootIndex++;
606
}
607
}
608
// We need the itemIds to notify, though once the switch to guids is
609
// complete we may stop using them.
610
let itemIdMap = await PlacesUtils.promiseManyItemIds(
611
insertInfos.map(info => info.guid)
612
);
613
614
let notifications = [];
615
for (let i = 0; i < insertInfos.length; i++) {
616
let item = insertInfos[i];
617
let itemId = itemIdMap.get(item.guid);
618
// For sub-folders, we need to make sure their children have the correct parent ids.
619
let parentId;
620
if (item.parentGuid === treeParent.guid) {
621
// This is a direct child of the tree parent, so we can use the
622
// existing parent's id.
623
parentId = treeParent._id;
624
} else {
625
// This is a parent folder that's been updated, so we need to
626
// use the new item id.
627
parentId = itemIdMap.get(item.parentGuid);
628
}
629
630
let url = "";
631
if (item.type == Bookmarks.TYPE_BOOKMARK) {
632
url = item.url instanceof URL ? item.url.href : item.url;
633
}
634
635
notifications.push(
636
new PlacesBookmarkAddition({
637
id: itemId,
638
url,
639
itemType: item.type,
640
parentId,
641
index: item.index,
642
title: item.title,
643
dateAdded: item.dateAdded,
644
guid: item.guid,
645
parentGuid: item.parentGuid,
646
source: item.source,
647
isTagging: false,
648
})
649
);
650
651
try {
652
await handleBookmarkItemSpecialData(itemId, item);
653
} catch (ex) {
654
// This is not critical, regardless the bookmark has been created
655
// and we should continue notifying the next ones.
656
Cu.reportError(
657
`An error occured while handling special bookmark data: ${ex}`
658
);
659
}
660
661
// Remove non-enumerable properties.
662
delete item.source;
663
664
insertInfos[i] = Object.assign({}, item);
665
}
666
667
PlacesObservers.notifyListeners(notifications);
668
669
return insertInfos;
670
})();
671
},
672
673
/**
674
* Updates a bookmark-item.
675
*
676
* Only set the properties which should be changed (undefined properties
677
* won't be taken into account).
678
* Moreover, the item's type or dateAdded cannot be changed, since they are
679
* immutable after creation. Trying to change them will reject.
680
*
681
* Note that any known properties that don't apply to the specific item type
682
* cause an exception.
683
*
684
* @param info
685
* object representing a bookmark-item, as defined above.
686
*
687
* @return {Promise} resolved when the update is complete.
688
* @resolves to an object representing the updated bookmark.
689
* @rejects if it's not possible to update the given bookmark.
690
* @throws if the arguments are invalid.
691
*/
692
update(info) {
693
// The info object is first validated here to ensure it's consistent, then
694
// it's compared to the existing item to remove any properties that don't
695
// need to be updated.
696
let updateInfo = validateBookmarkObject("Bookmarks.jsm: update", info, {
697
guid: { required: true },
698
index: {
699
requiredIf: b => b.hasOwnProperty("parentGuid"),
700
validIf: b => b.index >= 0 || b.index == this.DEFAULT_INDEX,
701
},
702
parentGuid: { validIf: b => b.parentGuid != this.rootGuid },
703
source: { defaultValue: this.SOURCES.DEFAULT },
704
});
705
706
// There should be at last one more property in addition to guid and source.
707
if (Object.keys(updateInfo).length < 3) {
708
throw new Error("Not enough properties to update");
709
}
710
711
return (async () => {
712
// Ensure the item exists.
713
let item = await fetchBookmark(updateInfo);
714
if (!item) {
715
throw new Error("No bookmarks found for the provided GUID");
716
}
717
if (updateInfo.hasOwnProperty("type") && updateInfo.type != item.type) {
718
throw new Error("The bookmark type cannot be changed");
719
}
720
721
// Remove any property that will stay the same.
722
removeSameValueProperties(updateInfo, item);
723
// Check if anything should still be updated.
724
if (Object.keys(updateInfo).length < 3) {
725
// Remove non-enumerable properties.
726
return Object.assign({}, item);
727
}
728
const now = new Date();
729
let lastModifiedDefault = now;
730
// In the case where `dateAdded` is specified, but `lastModified` is not,
731
// we only update `lastModified` if it is older than the new `dateAdded`.
732
if (!("lastModified" in updateInfo) && "dateAdded" in updateInfo) {
733
lastModifiedDefault = new Date(
734
Math.max(item.lastModified, updateInfo.dateAdded)
735
);
736
}
737
updateInfo = validateBookmarkObject("Bookmarks.jsm: update", updateInfo, {
738
url: { validIf: () => item.type == this.TYPE_BOOKMARK },
739
title: {
740
validIf: () =>
741
[this.TYPE_BOOKMARK, this.TYPE_FOLDER].includes(item.type),
742
},
743
lastModified: {
744
defaultValue: lastModifiedDefault,
745
validIf: b =>
746
b.lastModified >= now ||
747
b.lastModified >= (b.dateAdded || item.dateAdded),
748
},
749
dateAdded: { defaultValue: item.dateAdded },
750
});
751
752
return PlacesUtils.withConnectionWrapper(
753
"Bookmarks.jsm: update",
754
async db => {
755
let parent;
756
if (updateInfo.hasOwnProperty("parentGuid")) {
757
if (PlacesUtils.isRootItem(item.guid)) {
758
throw new Error("It's not possible to move Places root folders.");
759
}
760
if (item.type == this.TYPE_FOLDER) {
761
// Make sure we are not moving a folder into itself or one of its
762
// descendants.
763
let rows = await db.executeCached(
764
`WITH RECURSIVE
765
descendants(did) AS (
766
VALUES(:id)
767
UNION ALL
768
SELECT id FROM moz_bookmarks
769
JOIN descendants ON parent = did
770
WHERE type = :type
771
)
772
SELECT guid FROM moz_bookmarks
773
WHERE id IN descendants
774
`,
775
{ id: item._id, type: this.TYPE_FOLDER }
776
);
777
if (
778
rows
779
.map(r => r.getResultByName("guid"))
780
.includes(updateInfo.parentGuid)
781
) {
782
throw new Error(
783
"Cannot insert a folder into itself or one of its descendants"
784
);
785
}
786
}
787
788
parent = await fetchBookmark({ guid: updateInfo.parentGuid });
789
if (!parent) {
790
throw new Error("No bookmarks found for the provided parentGuid");
791
}
792
}
793
794
if (updateInfo.hasOwnProperty("index")) {
795
if (PlacesUtils.isRootItem(item.guid)) {
796
throw new Error("It's not possible to move Places root folders.");
797
}
798
// If at this point we don't have a parent yet, we are moving into
799
// the same container. Thus we know it exists.
800
if (!parent) {
801
parent = await fetchBookmark({ guid: item.parentGuid });
802
}
803
804
if (
805
updateInfo.index >= parent._childCount ||
806
updateInfo.index == this.DEFAULT_INDEX
807
) {
808
updateInfo.index = parent._childCount;
809
810
// Fix the index when moving within the same container.
811
if (parent.guid == item.parentGuid) {
812
updateInfo.index--;
813
}
814
}
815
}
816
817
let syncChangeDelta = PlacesSyncUtils.bookmarks.determineSyncChangeDelta(
818
info.source
819
);
820
821
let updatedItem = await db.executeTransaction(async function() {
822
let updatedItem = await updateBookmark(
823
db,
824
updateInfo,
825
item,
826
item.index,
827
parent,
828
syncChangeDelta
829
);
830
if (parent) {
831
await setAncestorsLastModified(
832
db,
833
parent.guid,
834
updatedItem.lastModified,
835
syncChangeDelta
836
);
837
}
838
return updatedItem;
839
});
840
841
if (
842
item.type == this.TYPE_BOOKMARK &&
843
item.url.href != updatedItem.url.href
844
) {
845
// ...though we don't wait for the calculation.
846
updateFrecency(db, [item.url]).catch(Cu.reportError);
847
updateFrecency(db, [updatedItem.url]).catch(Cu.reportError);
848
}
849
850
// Notify onItemChanged to listeners.
851
let observers = PlacesUtils.bookmarks.getObservers();
852
// For lastModified, we only care about the original input, since we
853
// should not notify implciit lastModified changes.
854
if (
855
info.hasOwnProperty("lastModified") &&
856
updateInfo.hasOwnProperty("lastModified") &&
857
item.lastModified != updatedItem.lastModified
858
) {
859
notify(observers, "onItemChanged", [
860
updatedItem._id,
861
"lastModified",
862
false,
863
`${PlacesUtils.toPRTime(updatedItem.lastModified)}`,
864
PlacesUtils.toPRTime(updatedItem.lastModified),
865
updatedItem.type,
866
updatedItem._parentId,
867
updatedItem.guid,
868
updatedItem.parentGuid,
869
"",
870
updatedItem.source,
871
]);
872
}
873
if (
874
info.hasOwnProperty("dateAdded") &&
875
updateInfo.hasOwnProperty("dateAdded") &&
876
item.dateAdded != updatedItem.dateAdded
877
) {
878
notify(observers, "onItemChanged", [
879
updatedItem._id,
880
"dateAdded",
881
false,
882
`${PlacesUtils.toPRTime(updatedItem.dateAdded)}`,
883
PlacesUtils.toPRTime(updatedItem.lastModified),
884
updatedItem.type,
885
updatedItem._parentId,
886
updatedItem.guid,
887
updatedItem.parentGuid,
888
"",
889
updatedItem.source,
890
]);
891
}
892
if (updateInfo.hasOwnProperty("title")) {
893
let isTagging = updatedItem.parentGuid == Bookmarks.tagsGuid;
894
notify(
895
observers,
896
"onItemChanged",
897
[
898
updatedItem._id,
899
"title",
900
false,
901
updatedItem.title,
902
PlacesUtils.toPRTime(updatedItem.lastModified),
903
updatedItem.type,
904
updatedItem._parentId,
905
updatedItem.guid,
906
updatedItem.parentGuid,
907
"",
908
updatedItem.source,
909
],
910
{ isTagging }
911
);
912
// If we're updating a tag, we must notify all the tagged bookmarks
913
// about the change.
914
if (isTagging) {
915
for (let entry of await fetchBookmarksByTags(
916
{ tags: [updatedItem.title] },
917
{ concurrent: true }
918
)) {
919
notify(observers, "onItemChanged", [
920
entry._id,
921
"tags",
922
false,
923
"",
924
PlacesUtils.toPRTime(entry.lastModified),
925
entry.type,
926
entry._parentId,
927
entry.guid,
928
entry.parentGuid,
929
"",
930
updatedItem.source,
931
]);
932
}
933
}
934
}
935
if (updateInfo.hasOwnProperty("url")) {
936
await PlacesUtils.keywords.reassign(
937
item.url,
938
updatedItem.url,
939
updatedItem.source
940
);
941
notify(observers, "onItemChanged", [
942
updatedItem._id,
943
"uri",
944
false,
945
updatedItem.url.href,
946
PlacesUtils.toPRTime(updatedItem.lastModified),
947
updatedItem.type,
948
updatedItem._parentId,
949
updatedItem.guid,
950
updatedItem.parentGuid,
951
item.url.href,
952
updatedItem.source,
953
]);
954
}
955
// If the item was moved, notify onItemMoved.
956
if (
957
item.parentGuid != updatedItem.parentGuid ||
958
item.index != updatedItem.index
959
) {
960
notify(observers, "onItemMoved", [
961
updatedItem._id,
962
item._parentId,
963
item.index,
964
updatedItem._parentId,
965
updatedItem.index,
966
updatedItem.type,
967
updatedItem.guid,
968
item.parentGuid,
969
updatedItem.parentGuid,
970
updatedItem.source,
971
updatedItem.url && updatedItem.url.href,
972
]);
973
}
974
975
// Remove non-enumerable properties.
976
delete updatedItem.source;
977
return Object.assign({}, updatedItem);
978
}
979
);
980
})();
981
},
982
983
/**
984
* Moves multiple bookmark-items to a specific folder.
985
*
986
* If you are only updating/moving a single bookmark, use update() instead.
987
*
988
* @param {Array} guids
989
* An array of GUIDs representing the bookmarks to move.
990
* @param {String} parentGuid
991
* Optional, the parent GUID to move the bookmarks to.
992
* @param {Integer} index
993
* The index to move the bookmarks to. If this is -1, the bookmarks
994
* will be appended to the folder.
995
* @param {Integer} source
996
* One of the Bookmarks.SOURCES.* options, representing the source of
997
* this change.
998
*
999
* @return {Promise} resolved when the move is complete.
1000
* @resolves to an array of objects representing the moved bookmarks.
1001
* @rejects if it's not possible to move the given bookmark(s).
1002
* @throws if the arguments are invalid.
1003
*/
1004
moveToFolder(guids, parentGuid, index, source) {
1005
if (!Array.isArray(guids) || guids.length < 1) {
1006
throw new Error("guids should be an array of at least one item");
1007
}
1008
if (!guids.every(guid => PlacesUtils.isValidGuid(guid))) {
1009
throw new Error("Expected only valid GUIDs to be passed.");
1010
}
1011
if (parentGuid && !PlacesUtils.isValidGuid(parentGuid)) {
1012
throw new Error("parentGuid should be a valid GUID");
1013
}
1014
if (parentGuid == PlacesUtils.bookmarks.rootGuid) {
1015
throw new Error("Cannot move bookmarks into root.");
1016
}
1017
if (typeof index != "number" || index < this.DEFAULT_INDEX) {
1018
throw new Error(
1019
`index should be a number greater than ${this.DEFAULT_INDEX}`
1020
);
1021
}
1022
1023
if (!source) {
1024
source = this.SOURCES.DEFAULT;
1025
}
1026
1027
return (async () => {
1028
let updateInfos = [];
1029
let syncChangeDelta = PlacesSyncUtils.bookmarks.determineSyncChangeDelta(
1030
source
1031
);
1032
1033
await PlacesUtils.withConnectionWrapper(
1034
"Bookmarks.jsm: moveToFolder",
1035
async db => {
1036
const lastModified = new Date();
1037
1038
let targetParentGuid = parentGuid || undefined;
1039
1040
for (let guid of guids) {
1041
// Ensure the item exists.
1042
let existingItem = await fetchBookmark({ guid }, { db });
1043
if (!existingItem) {
1044
throw new Error("No bookmarks found for the provided GUID");
1045
}
1046
1047
if (parentGuid) {
1048
// We're moving to a different folder.
1049
if (existingItem.type == this.TYPE_FOLDER) {
1050
// Make sure we are not moving a folder into itself or one of its
1051
// descendants.
1052
let rows = await db.executeCached(
1053
`WITH RECURSIVE
1054
descendants(did) AS (
1055
VALUES(:id)
1056
UNION ALL
1057
SELECT id FROM moz_bookmarks
1058
JOIN descendants ON parent = did
1059
WHERE type = :type
1060
)
1061
SELECT guid FROM moz_bookmarks
1062
WHERE id IN descendants
1063
`,
1064
{ id: existingItem._id, type: this.TYPE_FOLDER }
1065
);
1066
if (
1067
rows.map(r => r.getResultByName("guid")).includes(parentGuid)
1068
) {
1069
throw new Error(
1070
"Cannot insert a folder into itself or one of its descendants"
1071
);
1072
}
1073
}
1074
} else if (!targetParentGuid) {
1075
targetParentGuid = existingItem.parentGuid;
1076
} else if (existingItem.parentGuid != targetParentGuid) {
1077
throw new Error(
1078
"All bookmarks should be in the same folder if no parent is specified"
1079
);
1080
}
1081
1082
updateInfos.push({ existingItem, currIndex: existingItem.index });
1083
}
1084
1085
let newParent = await fetchBookmark(
1086
{ guid: targetParentGuid },
1087
{ db }
1088
);
1089
1090
if (newParent._grandParentId == PlacesUtils.tagsFolderId) {
1091
throw new Error("Can't move to a tags folder");
1092
}
1093
1094
let newParentChildCount = newParent._childCount;
1095
1096
await db.executeTransaction(async () => {
1097
// Now that we have all the existing items, we can do the actual updates.
1098
for (let i = 0; i < updateInfos.length; i++) {
1099
let info = updateInfos[i];
1100
if (index != this.DEFAULT_INDEX) {
1101
// If we're dropping on the same folder, then we may need to adjust
1102
// the index to insert at the correct place.
1103
if (info.existingItem.parentGuid == newParent.guid) {
1104
if (index > info.existingItem.index) {
1105
// If we're dragging down, we need to go one lower to insert at
1106
// the real point as moving the element changes the index of
1107
// everything below by 1.
1108
index--;
1109
} else if (index == info.existingItem.index) {
1110
// This isn't moving so we skip it, but copy the data so we have
1111
// an easy way for the notifications to check.
1112
info.updatedItem = { ...info.existingItem };
1113
continue;
1114
}
1115
}
1116
}
1117
1118
// Never let the index go higher than the max count of the folder.
1119
if (index == this.DEFAULT_INDEX || index >= newParentChildCount) {
1120
index = newParentChildCount;
1121
1122
// If this is moving within the same folder, then we need to drop the
1123
// index by one to compensate for "removing" it, then re-inserting.
1124
if (info.existingItem.parentGuid == newParent.guid) {
1125
index--;
1126
}
1127
}
1128
1129
info.updatedItem = await updateBookmark(
1130
db,
1131
{ lastModified, index },
1132
info.existingItem,
1133
info.currIndex,
1134
newParent,
1135
syncChangeDelta
1136
);
1137
1138
// For items moving within the same folder, we have to keep track
1139
// of their indexes. Otherwise we run the risk of not correctly
1140
// updating the indexes of other items in the folder.
1141
// This section simulates the database write in moveBookmark, which
1142
// allows us to avoid re-reading the database.
1143
if (info.existingItem.parentGuid == newParent.guid) {
1144
let sign = index < info.currIndex ? 1 : -1;
1145
for (let j = 0; j < updateInfos.length; j++) {
1146
if (j == i) {
1147
continue;
1148
}
1149
if (
1150
updateInfos[j].currIndex >=
1151
Math.min(info.currIndex, index) &&
1152
updateInfos[j].currIndex <= Math.max(info.currIndex, index)
1153
) {
1154
updateInfos[j].currIndex += sign;
1155
}
1156
}
1157
}
1158
info.currIndex = index;
1159
1160
// We only bump the parent count if we're moving from a different folder.
1161
if (info.existingItem.parentGuid != newParent.guid) {
1162
newParentChildCount++;
1163
}
1164
index++;
1165
}
1166
1167
await setAncestorsLastModified(
1168
db,
1169
newParent.guid,
1170
lastModified,
1171
syncChangeDelta
1172
);
1173
});
1174
}
1175
);
1176
1177
// Updates complete, time to notify everyone.
1178
for (let { updatedItem, existingItem } of updateInfos) {
1179
// Notify onItemChanged to listeners.
1180
let observers = PlacesUtils.bookmarks.getObservers();
1181
// If the item was moved, notify onItemMoved.
1182
// We use the updatedItem.index here, rather than currIndex, as the views
1183
// need to know where we inserted the item as opposed to where it ended
1184
// up.
1185
if (
1186
existingItem.parentGuid != updatedItem.parentGuid ||
1187
existingItem.index != updatedItem.index
1188
) {
1189
notify(observers, "onItemMoved", [
1190
updatedItem._id,
1191
existingItem._parentId,
1192
existingItem.index,
1193
updatedItem._parentId,
1194
updatedItem.index,
1195
updatedItem.type,
1196
updatedItem.guid,
1197
existingItem.parentGuid,
1198
updatedItem.parentGuid,
1199
source,
1200
existingItem.url,
1201
]);
1202
}
1203
// Remove non-enumerable properties.
1204
delete updatedItem.source;
1205
}
1206
1207
return updateInfos.map(updateInfo =>
1208
Object.assign({}, updateInfo.updatedItem)
1209
);
1210
})();
1211
},
1212
1213
/**
1214
* Removes one or more bookmark-items.
1215
*
1216
* @param guidOrInfo This may be:
1217
* - The globally unique identifier of the item to remove
1218
* - an object representing the item, as defined above
1219
* - an array of objects representing the items to be removed
1220
* @param {Object} [options={}]
1221
* Additional options that can be passed to the function.
1222
* Currently supports the following properties:
1223
* - preventRemovalOfNonEmptyFolders: Causes an exception to be
1224
* thrown when attempting to remove a folder that is not empty.
1225
* - source: The change source, forwarded to all bookmark observers.
1226
* Defaults to nsINavBookmarksService::SOURCE_DEFAULT.
1227
*
1228
* @return {Promise}
1229
* @resolves when the removal is complete
1230
* @rejects if the provided guid doesn't match any existing bookmark.
1231
* @throws if the arguments are invalid.
1232
*/
1233
remove(guidOrInfo, options = {}) {
1234
let infos = guidOrInfo;
1235
if (!infos) {
1236
throw new Error("Input should be a valid object");
1237
}
1238
if (!Array.isArray(guidOrInfo)) {
1239
if (typeof guidOrInfo != "object") {
1240
infos = [{ guid: guidOrInfo }];
1241
} else {
1242
infos = [guidOrInfo];
1243
}
1244
}
1245
1246
if (!("source" in options)) {
1247
options.source = Bookmarks.SOURCES.DEFAULT;
1248
}
1249
1250
let removeInfos = [];
1251
for (let info of infos) {
1252
// Disallow removing the root folders.
1253
if (
1254
[
1255
Bookmarks.rootGuid,
1256
Bookmarks.menuGuid,
1257
Bookmarks.toolbarGuid,
1258
Bookmarks.unfiledGuid,
1259
Bookmarks.tagsGuid,
1260
Bookmarks.mobileGuid,
1261
].includes(info.guid)
1262
) {
1263
throw new Error("It's not possible to remove Places root folders.");
1264
}
1265
1266
// Even if we ignore any other unneeded property, we still validate any
1267
// known property to reduce likelihood of hidden bugs.
1268
let removeInfo = validateBookmarkObject("Bookmarks.jsm: remove", info);
1269
removeInfos.push(removeInfo);
1270
}
1271
1272
return (async function() {
1273
let removeItems = [];
1274
for (let info of removeInfos) {
1275
// We must be able to remove a bookmark even if it has an invalid url.
1276
// In that case the item won't have a url property.
1277
let item = await fetchBookmark(info, { ignoreInvalidURLs: true });
1278
if (!item) {
1279
throw new Error("No bookmarks found for the provided GUID.");
1280
}
1281
1282
removeItems.push(item);
1283
}
1284
1285
await removeBookmarks(removeItems, options);
1286
1287
// Notify onItemRemoved to listeners.
1288
for (let item of removeItems) {
1289
let observers = PlacesUtils.bookmarks.getObservers();
1290
let uri = item.hasOwnProperty("url")
1291
? PlacesUtils.toURI(item.url)
1292
: null;
1293
let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
1294
notify(
1295
observers,
1296
"onItemRemoved",
1297
[
1298
item._id,
1299
item._parentId,
1300
item.index,
1301
item.type,
1302
uri,
1303
item.guid,
1304
item.parentGuid,
1305
options.source,
1306
],
1307
{ isTagging: isUntagging }
1308
);
1309
1310
if (isUntagging) {
1311
for (let entry of await fetchBookmarksByURL(item, {
1312
concurrent: true,
1313
})) {
1314
notify(observers, "onItemChanged", [
1315
entry._id,
1316
"tags",
1317
false,
1318
"",
1319
PlacesUtils.toPRTime(entry.lastModified),
1320
entry.type,
1321
entry._parentId,
1322
entry.guid,
1323
entry.parentGuid,
1324
"",
1325
options.source,
1326
]);
1327
}
1328
}
1329
}
1330
})();
1331
},
1332
1333
/**
1334
* Removes ALL bookmarks, resetting the bookmarks storage to an empty tree.
1335
*
1336
* Note that roots are preserved, only their children will be removed.
1337
*
1338
* @param {Object} [options={}]
1339
* Additional options. Currently supports the following properties:
1340
* - source: The change source, forwarded to all bookmark observers.
1341
* Defaults to nsINavBookmarksService::SOURCE_DEFAULT.
1342
*
1343
* @return {Promise} resolved when the removal is complete.
1344
* @resolves once the removal is complete.
1345
*/
1346
eraseEverything(options = {}) {
1347
if (!options.source) {
1348
options.source = Bookmarks.SOURCES.DEFAULT;
1349
}
1350
1351
return PlacesUtils.withConnectionWrapper(
1352
"Bookmarks.jsm: eraseEverything",
1353
async function(db) {
1354
let urls;
1355
1356
await db.executeTransaction(async function() {
1357
urls = await removeFoldersContents(
1358
db,
1359
Bookmarks.userContentRoots,
1360
options
1361
);
1362
const time = PlacesUtils.toPRTime(new Date());
1363
const syncChangeDelta = PlacesSyncUtils.bookmarks.determineSyncChangeDelta(
1364
options.source
1365
);
1366
for (let folderGuid of Bookmarks.userContentRoots) {
1367
await db.executeCached(
1368
`UPDATE moz_bookmarks SET lastModified = :time,
1369
syncChangeCounter = syncChangeCounter + :syncChangeDelta
1370
WHERE id IN (SELECT id FROM moz_bookmarks WHERE guid = :folderGuid )
1371
`,
1372
{ folderGuid, time, syncChangeDelta }
1373
);
1374
}
1375
1376
await PlacesSyncUtils.bookmarks.resetSyncMetadata(db, options.source);
1377
});
1378
1379
// We don't wait for the frecency calculation.
1380
if (urls && urls.length) {
1381
await PlacesUtils.keywords.eraseEverything();
1382
updateFrecency(db, urls, true).catch(Cu.reportError);
1383
}
1384
}
1385
);
1386
},
1387
1388
/**
1389
* Returns a list of recently bookmarked items.
1390
* Only includes actual bookmarks. Excludes folders, separators and queries.
1391
*
1392
* @param {integer} numberOfItems
1393
* The maximum number of bookmark items to return.
1394
*
1395
* @return {Promise} resolved when the listing is complete.
1396
* @resolves to an array of recent bookmark-items.
1397
* @rejects if an error happens while querying.
1398
*/
1399
getRecent(numberOfItems) {
1400
if (numberOfItems === undefined) {
1401
throw new Error("numberOfItems argument is required");
1402
}
1403
if (!typeof numberOfItems === "number" || numberOfItems % 1 !== 0) {
1404
throw new Error("numberOfItems argument must be an integer");
1405
}
1406
if (numberOfItems <= 0) {
1407
throw new Error("numberOfItems argument must be greater than zero");
1408
}
1409
1410
return fetchRecentBookmarks(numberOfItems);
1411
},
1412
1413
/**
1414
* Fetches information about a bookmark-item.
1415
*
1416
* REMARK: any successful call to this method resolves to a single
1417
* bookmark-item (or null), even when multiple bookmarks may exist
1418
* (e.g. fetching by url). If you wish to retrieve all of the
1419
* bookmarks for a given match, use the callback instead.
1420
*
1421
* Input can be either a guid or an object with one, and only one, of these
1422
* filtering properties set:
1423
* - guid
1424
* retrieves the item with the specified guid.
1425
* - parentGuid and index
1426
* retrieves the item by its position.
1427
* - url
1428
* retrieves the most recent bookmark having the given URL.
1429
* To retrieve ALL of the bookmarks for that URL, you must pass in an
1430
* onResult callback, that will be invoked once for each found bookmark.
1431
* - guidPrefix
1432
* retrieves the most recent item with the specified guid prefix.
1433
* To retrieve ALL of the bookmarks for that guid prefix, you must pass
1434
* in an onResult callback, that will be invoked once for each bookmark.
1435
* - tags
1436
* Retrieves the most recent item with all the specified tags.
1437
* The tags are matched in a case-insensitive way.
1438
* To retrieve ALL of the bookmarks having these tags, pass in an
1439
* onResult callback, that will be invoked once for each bookmark.
1440
* Note, there can be multiple bookmarks for the same url, if you need
1441
* unique tagged urls you can filter duplicates by accumulating in a Set.
1442
*
1443
* @param guidOrInfo
1444
* The globally unique identifier of the item to fetch, or an
1445
* object representing it, as defined above.
1446
* @param onResult [optional]
1447
* Callback invoked for each found bookmark.
1448
* @param options [optional]
1449
* an optional object whose properties describe options for the fetch:
1450
* - concurrent: fetches concurrently to any writes, returning results
1451
* faster. On the negative side, it may return stale
1452
* information missing the currently ongoing write.
1453
*
1454
* @return {Promise} resolved when the fetch is complete.
1455
* @resolves to an object representing the found item, as described above, or
1456
* an array of such objects. if no item is found, the returned
1457
* promise is resolved to null.
1458
* @rejects if an error happens while fetching.
1459
* @throws if the arguments are invalid.
1460
*
1461
* @note Any unknown property in the info object is ignored. Known properties
1462
* may be overwritten.
1463
*/
1464
fetch(guidOrInfo, onResult = null, options = {}) {
1465
if (onResult && typeof onResult != "function") {
1466
throw new Error("onResult callback must be a valid function");
1467
}
1468
let info = guidOrInfo;
1469
if (!info) {
1470
throw new Error("Input should be a valid object");
1471
}
1472
if (typeof info != "object") {
1473
info = { guid: guidOrInfo };
1474
} else if (Object.keys(info).length == 1) {
1475
// Just a faster code path.
1476
if (
1477
!["url", "guid", "parentGuid", "index", "guidPrefix", "tags"].includes(
1478
Object.keys(info)[0]
1479
)
1480
) {
1481
throw new Error(`Unexpected number of conditions provided: 0`);
1482
}
1483
} else {
1484
// Only one condition at a time can be provided.
1485
let conditionsCount = [
1486
v => v.hasOwnProperty("guid"),
1487
v => v.hasOwnProperty("parentGuid") && v.hasOwnProperty("index"),
1488
v => v.hasOwnProperty("url"),
1489
v => v.hasOwnProperty("guidPrefix"),
1490
v => v.hasOwnProperty("tags"),
1491
].reduce((old, fn) => (old + fn(info)) | 0, 0);
1492
if (conditionsCount != 1) {
1493
throw new Error(
1494
`Unexpected number of conditions provided: ${conditionsCount}`
1495
);
1496
}
1497
}
1498
1499
// Create a new options object with just the support properties, because
1500
// we may augment it and hand it down to other methods.
1501
options = {
1502
concurrent: !!options.concurrent,
1503
};
1504
1505
let behavior = {};
1506
if (info.hasOwnProperty("parentGuid") || info.hasOwnProperty("index")) {
1507
behavior = {
1508
parentGuid: { requiredIf: b => b.hasOwnProperty("index") },
1509
index: {
1510
requiredIf: b => b.hasOwnProperty("parentGuid"),
1511
validIf: b =>
1512
(typeof b.index == "number" && b.index >= 0) ||
1513
b.index == this.DEFAULT_INDEX,
1514
},
1515
};
1516
}
1517
1518
// Even if we ignore any other unneeded property, we still validate any
1519
// known property to reduce likelihood of hidden bugs.
1520
let fetchInfo = validateBookmarkObject(
1521
"Bookmarks.jsm: fetch",
1522
info,
1523
behavior
1524
);
1525
1526
return (async function() {
1527
let results;
1528
if (fetchInfo.hasOwnProperty("url")) {
1529
results = await fetchBookmarksByURL(fetchInfo, options);
1530
} else if (fetchInfo.hasOwnProperty("guid")) {
1531
results = await fetchBookmark(fetchInfo, options);
1532
} else if (
1533
fetchInfo.hasOwnProperty("parentGuid") &&
1534
fetchInfo.hasOwnProperty("index")
1535
) {
1536
results = await fetchBookmarkByPosition(fetchInfo, options);
1537
} else if (fetchInfo.hasOwnProperty("guidPrefix")) {
1538
results = await fetchBookmarksByGUIDPrefix(fetchInfo, options);
1539
} else if (fetchInfo.hasOwnProperty("tags")) {
1540
results = await fetchBookmarksByTags(fetchInfo, options);
1541
}
1542
1543
if (!results) {
1544
return null;
1545
}
1546
1547
if (!Array.isArray(results)) {
1548
results = [results];
1549
}
1550
// Remove non-enumerable properties.
1551
results = results.map(r => Object.assign({}, r));
1552
1553
// Ideally this should handle an incremental behavior and thus be invoked
1554
// while we fetch. Though, the likelihood of 2 or more bookmarks for the
1555
// same match is very low, so it's not worth the added code complication.
1556
if (onResult) {
1557
for (let result of results) {
1558
try {
1559
onResult(result);
1560
} catch (ex) {
1561
Cu.reportError(ex);
1562
}
1563
}
1564
}
1565
1566
return results[0];
1567
})();
1568
},
1569
1570
/**
1571
* Retrieves an object representation of a bookmark-item, along with all of
1572
* its descendants, if any.
1573
*
1574
* Each node in the tree is an object that extends the item representation
1575
* described above with some additional properties:
1576
*
1577
* - [deprecated] id (number)
1578
* the item's id. Defined only if aOptions.includeItemIds is set.
1579
* - annos (array)
1580
* the item's annotations. This is not set if there are no annotations
1581
* set for the item.
1582
*
1583
* The root object of the tree also has the following properties set:
1584
* - itemsCount (number, not enumerable)
1585
* the number of items, including the root item itself, which are
1586
* represented in the resolved object.
1587
*
1588
* Bookmarked URLs may also have the following properties:
1589
* - tags (string)
1590
* csv string of the bookmark's tags, if any.
1591
* - charset (string)
1592
* the last known charset of the bookmark, if any.
1593
* - iconurl (URL)
1594
* the bookmark's favicon URL, if any.
1595
*
1596
* Folders may also have the following properties:
1597
* - children (array)
1598
* the folder's children information, each of them having the same set of
1599
* properties as above.
1600
*
1601
* @param [optional] guid
1602
* the topmost item to be queried. If it's not passed, the Places
1603
* root folder is queried: that is, you get a representation of the
1604
* entire bookmarks hierarchy.
1605
* @param [optional] options
1606
* Options for customizing the query behavior, in the form of an
1607
* object with any of the following properties:
1608
* - excludeItemsCallback: a function for excluding items, along with
1609
* their descendants. Given an item object (that has everything set
1610
* apart its potential children data), it should return true if the
1611
* item should be excluded. Once an item is excluded, the function
1612
* isn't called for any of its descendants. This isn't called for
1613
* the root item.
1614
* WARNING: since the function may be called for each item, using
1615
* this option can slow down the process significantly if the
1616
* callback does anything that's not relatively trivial. It is
1617
* highly recommended to avoid any synchronous I/O or DB queries.
1618
* - includeItemIds: opt-in to include the deprecated id property.
1619
* Use it if you must. It'll be removed once the switch to guids is
1620
* complete.
1621
*
1622
* @return {Promise} resolved when the fetch is complete.
1623
* @resolves to an object that represents either a single item or a
1624
* bookmarks tree. if guid points to a non-existent item, the
1625
* returned promise is resolved to null.
1626
* @rejects if an error happens while fetching.
1627
* @throws if the arguments are invalid.
1628
*/
1629
// TODO must implement these methods yet:
1630
// PlacesUtils.promiseBookmarksTree()
1631
fetchTree(guid = "", options = {}) {
1632
throw new Error("Not yet implemented");
1633
},
1634
1635
/**
1636
* Fetch all the existing tags, sorted alphabetically.
1637
* @return {Promise} resolves to an array of objects representing tags, when
1638
* fetching is complete.
1639
* Each object looks like {
1640
* name: the name of the tag,
1641
* count: number of bookmarks with this tag
1642
* }
1643
*/
1644
async fetchTags() {
1645
// TODO: Once the tagging API is implemented in Bookmarks.jsm, we can cache
1646
// the list of tags, instead of querying every time.
1647
let db = await PlacesUtils.promiseDBConnection();
1648
let rows = await db.executeCached(
1649
`
1650
SELECT b.title AS name, count(*) AS count
1651
FROM moz_bookmarks b
1652
JOIN moz_bookmarks p ON b.parent = p.id
1653
JOIN moz_bookmarks c ON c.parent = b.id
1654
WHERE p.guid = :tagsGuid
1655
GROUP BY name
1656
ORDER BY name COLLATE nocase ASC
1657
`,
1658
{ tagsGuid: this.tagsGuid }
1659
);
1660
return rows.map(r => ({
1661
name: r.getResultByName("name"),
1662
count: r.getResultByName("count"),
1663
}));
1664
},
1665
1666
/**
1667
* Reorders contents of a folder based on a provided array of GUIDs.
1668
*
1669
* @param parentGuid
1670
* The globally unique identifier of the folder whose contents should
1671
* be reordered.
1672
* @param orderedChildrenGuids
1673
* Ordered array of the children's GUIDs. If this list contains
1674
* non-existing entries they will be ignored. If the list is
1675
* incomplete, and the current child list is already in order with
1676
* respect to orderedChildrenGuids, no change is made. Otherwise, the
1677
* new items are appended but maintain their current order relative to
1678
* eachother.
1679
* @param {Object} [options={}]
1680
* Additional options. Currently supports the following properties:
1681
* - lastModified: The last modified time to use for the folder and
1682
reordered children. Defaults to the current time.
1683
* - source: The change source, forwarded to all bookmark observers.
1684
* Defaults to nsINavBookmarksService::SOURCE_DEFAULT.
1685
*
1686
* @return {Promise} resolved when reordering is complete.
1687
* @rejects if an error happens while reordering.
1688
* @throws if the arguments are invalid.
1689
*/
1690
reorder(parentGuid, orderedChildrenGuids, options = {}) {
1691
let info = { guid: parentGuid };
1692
info = validateBookmarkObject("Bookmarks.jsm: reorder", info, {
1693
guid: { required: true },
1694
});
1695
1696
if (!Array.isArray(orderedChildrenGuids) || !orderedChildrenGuids.length) {
1697
throw new Error("Must provide a sorted array of children GUIDs.");
1698
}
1699
try {
1700
orderedChildrenGuids.forEach(PlacesUtils.BOOKMARK_VALIDATORS.guid);
1701
} catch (ex) {
1702
throw new Error("Invalid GUID found in the sorted children array.");
1703
}
1704
1705
options.source =
1706
"source" in options
1707
? PlacesUtils.BOOKMARK_VALIDATORS.source(options.source)
1708
: Bookmarks.SOURCES.DEFAULT;
1709
options.lastModified =
1710
"lastModified" in options
1711
? PlacesUtils.BOOKMARK_VALIDATORS.lastModified(options.lastModified)
1712
: new Date();
1713
1714
return (async () => {
1715
let parent = await fetchBookmark(info);
1716
if (!parent || parent.type != this.TYPE_FOLDER) {
1717
throw new Error("No folder found for the provided GUID.");
1718
}
1719
1720
let sortedChildren = await reorderChildren(
1721
parent,
1722
orderedChildrenGuids,
1723
options
1724
);
1725
1726
let observers = PlacesUtils.bookmarks.getObservers();
1727
// Note that child.index is the old index.
1728
for (let i = 0; i < sortedChildren.length; ++i) {
1729
let child = sortedChildren[i];
1730
notify(observers, "onItemMoved", [
1731
child._id,
1732
child._parentId,
1733
child.index,
1734
child._parentId,
1735
i,
1736
child.type,
1737
child.guid,
1738
child.parentGuid,
1739
child.parentGuid,
1740
options.source,
1741
child.url && child.url.href,
1742
]);
1743
}
1744
})();
1745
},
1746
1747
/**
1748
* Searches a list of bookmark-items by a search term, url or title.
1749
*
1750
* IMPORTANT:
1751
* This is intended as an interim API for the web-extensions implementation.
1752
* It will be removed as soon as we have a new querying API.
1753
*
1754
* Note also that this used to exclude separators but no longer does so.
1755
*
1756
* If you just want to search bookmarks by URL, use .fetch() instead.
1757
*
1758
* @param query
1759
* Either a string to use as search term, or an object
1760
* containing any of these keys: query, title or url with the
1761
* corresponding string to match as value.
1762
* The url property can be either a string or an nsIURI.
1763
*
1764
* @return {Promise} resolved when the search is complete.
1765
* @resolves to an array of found bookmark-items.
1766
* @rejects if an error happens while searching.
1767
* @throws if the arguments are invalid.
1768
*
1769
* @note Any unknown property in the query object is ignored.
1770
* Known properties may be overwritten.
1771
*/
1772
search(query) {
1773
if (!query) {
1774
throw new Error("Query object is required");
1775
}
1776
if (typeof query === "string") {
1777
query = { query };
1778
}
1779
if (typeof query !== "object") {
1780
throw new Error("Query must be an object or a string");
1781
}
1782
if (query.query && typeof query.query !== "string") {
1783
throw new Error("Query option must be a string");
1784
}
1785
if (query.title && typeof query.title !== "string") {
1786
throw new Error("Title option must be a string");
1787
}
1788
1789
if (query.url) {
1790
if (typeof query.url === "string" || query.url instanceof URL) {
1791
query.url = new URL(query.url).href;
1792
} else if (query.url instanceof Ci.nsIURI) {
1793
query.url = query.url.spec;
1794
} else {
1795
throw new Error("Url option must be a string or a URL object");
1796
}
1797
}
1798
1799
return queryBookmarks(query);
1800
},
1801
});
1802
1803
// Globals.
1804
1805
/**
1806
* Sends a bookmarks notification through the given observers.
1807
*
1808
* @param {Array} observers
1809
* array of nsINavBookmarkObserver objects.
1810
* @param {String} notification
1811
* the notification name.
1812
* @param {Array} [args]
1813
* array of arguments to pass to the notification.
1814
* @param {Object} [information]
1815
* Information about the notification, so we can filter based
1816
* based on the observer's preferences.
1817
*/
1818
function notify(observers, notification, args = [], information = {}) {
1819
for (let observer of observers) {
1820
if (information.isTagging && observer.skipTags) {
1821
continue;
1822
}
1823
1824
if (
1825
information.isDescendantRemoval &&
1826
observer.skipDescendantsOnItemRemoval &&
1827
!PlacesUtils.bookmarks.userContentRoots.includes(information.parentGuid)
1828
) {
1829
continue;
1830
}
1831
1832
try {
1833
observer[notification](...args);
1834
} catch (ex) {}
1835
}
1836
}
1837
1838
// Update implementation.
1839
1840
/**
1841
* Updates a single bookmark in the database. This should be called from within
1842
* a transaction.
1843
*
1844
* @param {Object} db The pre-existing database connection.
1845
* @param {Object} info A bookmark-item structure with new properties.
1846
* @param {Object} item A bookmark-item structure representing the existing bookmark.
1847
* @param {Integer} oldIndex The index of the item in the old parent.
1848
* @param {Object} newParent The new parent folder (note: this may be the same as)
1849
* the existing folder.
1850
* @param {Integer} syncChangeDelta The change delta to be applied.
1851
*/
1852
async function updateBookmark(
1853
db,
1854
info,
1855
item,
1856
oldIndex,
1857
newParent,
1858
syncChangeDelta
1859
) {
1860
let tuples = new Map();
1861
tuples.set("lastModified", {
1862
value: PlacesUtils.toPRTime(info.lastModified),
1863
});
1864
if (info.hasOwnProperty("title")) {
1865
tuples.set("title", {
1866
value: info.title,
1867
fragment: `title = NULLIF(:title, '')`,
1868
});
1869
}
1870
if (info.hasOwnProperty("dateAdded")) {
1871
tuples.set("dateAdded", { value: PlacesUtils.toPRTime(info.dateAdded) });
1872
}
1873
1874
if (info.hasOwnProperty("url")) {
1875
// Ensure a page exists in moz_places for this URL.
1876
await maybeInsertPlace(db, info.url);
1877
// Update tuples for the update query.
1878
tuples.set("url", {
1879
value: info.url.href,
1880
fragment:
1881
"fk = (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url)",
1882
});
1883
}
1884
1885
let newIndex = info.hasOwnProperty("index") ? info.index : item.index;
1886
if (newParent) {
1887
// For simplicity, update the index regardless.
1888
tuples.set("position", { value: newIndex });
1889
1890
// For moving within the same parent, we've already updated the indexes.
1891
if (newParent.guid == item.parentGuid) {
1892
// Moving inside the original container.
1893
// When moving "up", add 1 to each index in the interval.
1894
// Otherwise when moving down, we subtract 1.
1895
// Only the parent needs a sync change, which is handled in
1896
// `setAncestorsLastModified`.
1897
await db.executeCached(
1898
`UPDATE moz_bookmarks
1899
SET position = CASE WHEN :newIndex < :currIndex
1900
THEN position + 1
1901
ELSE position - 1
1902
END
1903
WHERE parent = :newParentId
1904
AND position BETWEEN :lowIndex AND :highIndex
1905
`,
1906
{
1907
newIndex,
1908
currIndex: oldIndex,
1909
newParentId: newParent._id,
1910
lowIndex: Math.min(oldIndex, newIndex),
1911
highIndex: Math.max(oldIndex, newIndex),
1912
}
1913
);
1914
} else {
1915
// Moving across different containers. In this case, both parents and
1916
// the child need sync changes. `setAncestorsLastModified`, below and in
1917
// `update` and `moveToFolder`, handles the parents. The `needsSyncChange`
1918
// check below handles the child.
1919
tuples.set("parent", { value: newParent._id });
1920
await db.executeCached(
1921
`UPDATE moz_bookmarks SET position = position - 1
1922
WHERE parent = :oldParentId
1923
AND position >= :oldIndex
1924
`,
1925
{ oldParentId: item._parentId, oldIndex }
1926
);
1927
await db.executeCached(
1928
`UPDATE moz_bookmarks SET position = position + 1
1929
WHERE parent = :newParentId
1930
AND position >= :newIndex
1931
`,
1932
{ newParentId: newParent._id, newIndex }
1933
);
1934
1935
await setAncestorsLastModified(
1936
db,
1937
item.parentGuid,
1938
info.lastModified,
1939
syncChangeDelta
1940
);
1941
}
1942
}
1943
1944
if (syncChangeDelta) {
1945
// Sync stores child indices in the parent's record, so we only bump the
1946
// item's counter if we're updating at least one more property in
1947
// addition to the index, last modified time, and dateAdded.
1948
let sizeThreshold = 1;
1949
if (newIndex != oldIndex) {
1950
++sizeThreshold;
1951
}
1952
if (tuples.has("dateAdded")) {
1953
++sizeThreshold;
1954
}
1955
let needsSyncChange = tuples.size > sizeThreshold;
1956
if (needsSyncChange) {
1957
tuples.set("syncChangeDelta", {
1958
value: syncChangeDelta,
1959
fragment: "syncChangeCounter = syncChangeCounter + :syncChangeDelta",
1960
});
1961
}
1962
}
1963
1964
let isTagging = item._grandParentId == PlacesUtils.tagsFolderId;
1965
if (isTagging) {
1966
// If we're updating a tag entry, bump the sync change counter for
1967
// bookmarks with the tagged URL.
1968
await PlacesSyncUtils.bookmarks.addSyncChangesForBookmarksWithURL(
1969
db,
1970
item.url,
1971
syncChangeDelta
1972
);
1973
if (info.hasOwnProperty("url")) {
1974
// Changing the URL of a tag entry is equivalent to untagging the
1975
// old URL and tagging the new one, so we bump the change counter
1976
// for the new URL here.
1977
await PlacesSyncUtils.bookmarks.addSyncChangesForBookmarksWithURL(
1978
db,
1979
info.url,
1980
syncChangeDelta
1981
);
1982
}
1983
}
1984
1985
let isChangingTagFolder = item._parentId == PlacesUtils.tagsFolderId;
1986
if (isChangingTagFolder && syncChangeDelta) {
1987
// If we're updating a tag folder (for example, changing a tag's title),
1988
// bump the change counter for all tagged bookmarks.
1989
await db.executeCached(
1990
`
1991
UPDATE moz_bookmarks SET
1992
syncChangeCounter = syncChangeCounter + :syncChangeDelta
1993
WHERE type = :type AND
1994
fk = (SELECT fk FROM moz_bookmarks WHERE parent = :parent)
1995
`,
1996
{ syncChangeDelta, type: Bookmarks.TYPE_BOOKMARK, parent: item._id }
1997
);
1998
}
1999
2000
await db.executeCached(
2001
`UPDATE moz_bookmarks
2002
SET ${Array.from(tuples.keys())
2003
.map(v => tuples.get(v).fragment || `${v} = :${v}`)
2004
.join(", ")}
2005
WHERE guid = :guid
2006
`,
2007
Object.assign(
2008
{ guid: item.guid },
2009
[...tuples.entries()].reduce((p, c) => {
2010
p[c[0]] = c[1].value;
2011
return p;
2012
}, {})
2013
)
2014
);
2015
2016
if (newParent) {
2017
if (newParent.guid == item.parentGuid) {
2018
// Mark all affected separators as changed
2019
// Also bumps the change counter if the item itself is a separator
2020
const startIndex = Math.min(newIndex, oldIndex);
2021
await adjustSeparatorsSyncCounter(
2022
db,
2023
newParent._id,
2024
startIndex,
2025
syncChangeDelta
2026
);
2027
} else {
2028
// Mark all affected separators as changed
2029
await adjustSeparatorsSyncCounter(
2030
db,
2031
item._parentId,
2032
oldIndex,
2033
syncChangeDelta
2034
);
2035
await adjustSeparatorsSyncCounter(
2036
db,
2037
newParent._id,
2038
newIndex,
2039
syncChangeDelta
2040
);