Source code

Revision control

Other Tools

1
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
2
/* This Source Code Form is subject to the terms of the Mozilla Public
3
* License, v. 2.0. If a copy of the MPL was not distributed with this
4
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6
var EXPORTED_SYMBOLS = ["PlacesUtils"];
7
8
const { XPCOMUtils } = ChromeUtils.import(
10
);
11
const { AppConstants } = ChromeUtils.import(
13
);
14
15
XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
16
17
XPCOMUtils.defineLazyModuleGetters(this, {
24
});
25
26
XPCOMUtils.defineLazyGetter(this, "MOZ_ACTION_REGEX", () => {
27
return /^moz-action:([^,]+),(.*)$/;
28
});
29
30
// On Mac OSX, the transferable system converts "\r\n" to "\n\n", where
31
// we really just want "\n". On other platforms, the transferable system
32
// converts "\r\n" to "\n".
33
const NEWLINE = AppConstants.platform == "macosx" ? "\n" : "\r\n";
34
35
// Timers resolution is not always good, it can have a 16ms precision on Win.
36
const TIMERS_RESOLUTION_SKEW_MS = 16;
37
38
function QI_node(aNode, aIID) {
39
try {
40
return aNode.QueryInterface(aIID);
41
} catch (ex) {}
42
return null;
43
}
44
function asContainer(aNode) {
45
return QI_node(aNode, Ci.nsINavHistoryContainerResultNode);
46
}
47
function asQuery(aNode) {
48
return QI_node(aNode, Ci.nsINavHistoryQueryResultNode);
49
}
50
51
/**
52
* Sends a bookmarks notification through the given observers.
53
*
54
* @param observers
55
* array of nsINavBookmarkObserver objects.
56
* @param notification
57
* the notification name.
58
* @param args
59
* array of arguments to pass to the notification.
60
*/
61
function notify(observers, notification, args) {
62
for (let observer of observers) {
63
try {
64
observer[notification](...args);
65
} catch (ex) {}
66
}
67
}
68
69
/**
70
* Sends a keyword change notification.
71
*
72
* @param url
73
* the url to notify about.
74
* @param keyword
75
* The keyword to notify, or empty string if a keyword was removed.
76
*/
77
async function notifyKeywordChange(url, keyword, source) {
78
// Notify bookmarks about the removal.
79
let bookmarks = [];
80
await PlacesUtils.bookmarks.fetch({ url }, b => bookmarks.push(b));
81
for (let bookmark of bookmarks) {
82
let ids = await PlacesUtils.promiseManyItemIds([
83
bookmark.guid,
84
bookmark.parentGuid,
85
]);
86
bookmark.id = ids.get(bookmark.guid);
87
bookmark.parentId = ids.get(bookmark.parentGuid);
88
}
89
let observers = PlacesUtils.bookmarks.getObservers();
90
for (let bookmark of bookmarks) {
91
notify(observers, "onItemChanged", [
92
bookmark.id,
93
"keyword",
94
false,
95
keyword,
96
bookmark.lastModified * 1000,
97
bookmark.type,
98
bookmark.parentId,
99
bookmark.guid,
100
bookmark.parentGuid,
101
"",
102
source,
103
]);
104
}
105
}
106
107
/**
108
* Serializes the given node in JSON format.
109
*
110
* @param aNode
111
* An nsINavHistoryResultNode
112
*/
113
function serializeNode(aNode) {
114
let data = {};
115
116
data.title = aNode.title;
117
// The id is no longer used for copying within the same instance/session of
118
// Firefox as of at least 61. However, we keep the id for now to maintain
119
// backwards compat of drag and drop with older Firefox versions.
120
data.id = aNode.itemId;
121
data.itemGuid = aNode.bookmarkGuid;
122
// Add an instanceId so we can tell which instance of an FF session the data
123
// is coming from.
124
data.instanceId = PlacesUtils.instanceId;
125
126
let guid = aNode.bookmarkGuid;
127
128
// Some nodes, e.g. the unfiled/menu/toolbar ones can have a virtual guid, so
129
// we ignore any that are a folder shortcut. These will be handled below.
130
if (
131
guid &&
132
!PlacesUtils.bookmarks.isVirtualRootItem(guid) &&
133
!PlacesUtils.isVirtualLeftPaneItem(guid)
134
) {
135
if (aNode.parent) {
136
data.parent = aNode.parent.itemId;
137
data.parentGuid = aNode.parent.bookmarkGuid;
138
}
139
140
data.dateAdded = aNode.dateAdded;
141
data.lastModified = aNode.lastModified;
142
}
143
144
if (PlacesUtils.nodeIsURI(aNode)) {
145
// Check for url validity.
146
new URL(aNode.uri);
147
data.type = PlacesUtils.TYPE_X_MOZ_PLACE;
148
data.uri = aNode.uri;
149
if (aNode.tags) {
150
data.tags = aNode.tags;
151
}
152
} else if (PlacesUtils.nodeIsFolder(aNode)) {
153
if (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT) {
154
data.type = PlacesUtils.TYPE_X_MOZ_PLACE;
155
data.uri = aNode.uri;
156
data.concreteId = PlacesUtils.getConcreteItemId(aNode);
157
data.concreteGuid = PlacesUtils.getConcreteItemGuid(aNode);
158
} else {
159
data.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
160
}
161
} else if (PlacesUtils.nodeIsQuery(aNode)) {
162
data.type = PlacesUtils.TYPE_X_MOZ_PLACE;
163
data.uri = aNode.uri;
164
} else if (PlacesUtils.nodeIsSeparator(aNode)) {
165
data.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
166
}
167
168
return JSON.stringify(data);
169
}
170
171
// Imposed to limit database size.
172
const DB_URL_LENGTH_MAX = 65536;
173
const DB_TITLE_LENGTH_MAX = 4096;
174
const DB_DESCRIPTION_LENGTH_MAX = 256;
175
176
/**
177
* Executes a boolean validate function, throwing if it returns false.
178
*
179
* @param boolValidateFn
180
* A boolean validate function.
181
* @return the input value.
182
* @throws if input doesn't pass the validate function.
183
*/
184
function simpleValidateFunc(boolValidateFn) {
185
return (v, input) => {
186
if (!boolValidateFn(v, input)) {
187
throw new Error("Invalid value");
188
}
189
return v;
190
};
191
}
192
193
/**
194
* List of bookmark object validators, one per each known property.
195
* Validators must throw if the property value is invalid and return a fixed up
196
* version of the value, if needed.
197
*/
198
const BOOKMARK_VALIDATORS = Object.freeze({
199
guid: simpleValidateFunc(v => PlacesUtils.isValidGuid(v)),
200
parentGuid: simpleValidateFunc(v => PlacesUtils.isValidGuid(v)),
201
guidPrefix: simpleValidateFunc(v => PlacesUtils.isValidGuidPrefix(v)),
202
index: simpleValidateFunc(
203
v => Number.isInteger(v) && v >= PlacesUtils.bookmarks.DEFAULT_INDEX
204
),
205
dateAdded: simpleValidateFunc(v => v.constructor.name == "Date"),
206
lastModified: simpleValidateFunc(v => v.constructor.name == "Date"),
207
type: simpleValidateFunc(
208
v =>
209
Number.isInteger(v) &&
210
[
211
PlacesUtils.bookmarks.TYPE_BOOKMARK,
212
PlacesUtils.bookmarks.TYPE_FOLDER,
213
PlacesUtils.bookmarks.TYPE_SEPARATOR,
214
].includes(v)
215
),
216
title: v => {
217
if (v === null) {
218
return "";
219
}
220
if (typeof v == "string") {
221
return v.slice(0, DB_TITLE_LENGTH_MAX);
222
}
223
throw new Error("Invalid title");
224
},
225
url: v => {
226
simpleValidateFunc(
227
val =>
228
(typeof val == "string" && val.length <= DB_URL_LENGTH_MAX) ||
229
(val instanceof Ci.nsIURI && val.spec.length <= DB_URL_LENGTH_MAX) ||
230
(val instanceof URL && val.href.length <= DB_URL_LENGTH_MAX)
231
).call(this, v);
232
if (typeof v === "string") {
233
return new URL(v);
234
}
235
if (v instanceof Ci.nsIURI) {
236
return new URL(v.spec);
237
}
238
return v;
239
},
240
source: simpleValidateFunc(
241
v =>
242
Number.isInteger(v) &&
243
Object.values(PlacesUtils.bookmarks.SOURCES).includes(v)
244
),
245
keyword: simpleValidateFunc(v => typeof v == "string" && v.length),
246
charset: simpleValidateFunc(v => typeof v == "string" && v.length),
247
postData: simpleValidateFunc(v => typeof v == "string" && v.length),
248
tags: simpleValidateFunc(
249
v =>
250
Array.isArray(v) &&
251
v.length &&
252
v.every(item => item && typeof item == "string")
253
),
254
});
255
256
// Sync bookmark records can contain additional properties.
257
const SYNC_BOOKMARK_VALIDATORS = Object.freeze({
258
// Sync uses Places GUIDs for all records except roots.
259
recordId: simpleValidateFunc(
260
v =>
261
typeof v == "string" &&
262
(PlacesSyncUtils.bookmarks.ROOTS.includes(v) ||
263
PlacesUtils.isValidGuid(v))
264
),
265
parentRecordId: v => SYNC_BOOKMARK_VALIDATORS.recordId(v),
266
// Sync uses kinds instead of types.
267
kind: simpleValidateFunc(
268
v =>
269
typeof v == "string" &&
270
Object.values(PlacesSyncUtils.bookmarks.KINDS).includes(v)
271
),
272
query: simpleValidateFunc(v => v === null || (typeof v == "string" && v)),
273
folder: simpleValidateFunc(
274
v =>
275
typeof v == "string" &&
276
v &&
277
v.length <= PlacesUtils.bookmarks.MAX_TAG_LENGTH
278
),
279
tags: v => {
280
if (v === null) {
281
return [];
282
}
283
if (!Array.isArray(v)) {
284
throw new Error("Invalid tag array");
285
}
286
for (let tag of v) {
287
if (
288
typeof tag != "string" ||
289
!tag ||
290
tag.length > PlacesUtils.bookmarks.MAX_TAG_LENGTH
291
) {
292
throw new Error(`Invalid tag: ${tag}`);
293
}
294
}
295
return v;
296
},
297
keyword: simpleValidateFunc(v => v === null || typeof v == "string"),
298
dateAdded: simpleValidateFunc(
299
v =>
300
typeof v === "number" &&
301
v > PlacesSyncUtils.bookmarks.EARLIEST_BOOKMARK_TIMESTAMP
302
),
303
feed: v => (v === null ? v : BOOKMARK_VALIDATORS.url(v)),
304
site: v => (v === null ? v : BOOKMARK_VALIDATORS.url(v)),
305
title: BOOKMARK_VALIDATORS.title,
306
url: BOOKMARK_VALIDATORS.url,
307
});
308
309
// Sync change records are passed between `PlacesSyncUtils` and the Sync
310
// bookmarks engine, and are used to update an item's sync status and change
311
// counter at the end of a sync.
312
const SYNC_CHANGE_RECORD_VALIDATORS = Object.freeze({
313
modified: simpleValidateFunc(v => typeof v == "number" && v >= 0),
314
counter: simpleValidateFunc(v => typeof v == "number" && v >= 0),
315
status: simpleValidateFunc(
316
v =>
317
typeof v == "number" &&
318
Object.values(PlacesUtils.bookmarks.SYNC_STATUS).includes(v)
319
),
320
tombstone: simpleValidateFunc(v => v === true || v === false),
321
synced: simpleValidateFunc(v => v === true || v === false),
322
});
323
/**
324
* List PageInfo bookmark object validators.
325
*/
326
const PAGEINFO_VALIDATORS = Object.freeze({
327
guid: BOOKMARK_VALIDATORS.guid,
328
url: BOOKMARK_VALIDATORS.url,
329
title: v => {
330
if (v == null || v == undefined) {
331
return undefined;
332
} else if (typeof v === "string") {
333
return v;
334
}
335
throw new TypeError(
336
`title property of PageInfo object: ${v} must be a string if provided`
337
);
338
},
339
previewImageURL: v => {
340
if (!v) {
341
return null;
342
}
343
return BOOKMARK_VALIDATORS.url(v);
344
},
345
description: v => {
346
if (typeof v === "string" || v === null) {
347
return v ? v.slice(0, DB_DESCRIPTION_LENGTH_MAX) : null;
348
}
349
throw new TypeError(
350
`description property of pageInfo object: ${v} must be either a string or null if provided`
351
);
352
},
353
annotations: v => {
354
if (typeof v != "object" || v.constructor.name != "Map") {
355
throw new TypeError("annotations must be a Map");
356
}
357
358
if (v.size == 0) {
359
throw new TypeError("there must be at least one annotation");
360
}
361
362
for (let [key, value] of v.entries()) {
363
if (typeof key != "string") {
364
throw new TypeError("all annotation keys must be strings");
365
}
366
if (
367
typeof value != "string" &&
368
typeof value != "number" &&
369
typeof value != "boolean" &&
370
value !== null &&
371
value !== undefined
372
) {
373
throw new TypeError(
374
"all annotation values must be Boolean, Numbers or Strings"
375
);
376
}
377
}
378
return v;
379
},
380
visits: v => {
381
if (!Array.isArray(v) || !v.length) {
382
throw new TypeError("PageInfo object must have an array of visits");
383
}
384
let visits = [];
385
for (let inVisit of v) {
386
let visit = {
387
date: new Date(),
388
transition: inVisit.transition || History.TRANSITIONS.LINK,
389
};
390
391
if (!PlacesUtils.history.isValidTransition(visit.transition)) {
392
throw new TypeError(
393
`transition: ${visit.transition} is not a valid transition type`
394
);
395
}
396
397
if (inVisit.date) {
398
PlacesUtils.history.ensureDate(inVisit.date);
399
if (inVisit.date > Date.now() + TIMERS_RESOLUTION_SKEW_MS) {
400
throw new TypeError(`date: ${inVisit.date} cannot be a future date`);
401
}
402
visit.date = inVisit.date;
403
}
404
405
if (inVisit.referrer) {
406
visit.referrer = PlacesUtils.normalizeToURLOrGUID(inVisit.referrer);
407
}
408
visits.push(visit);
409
}
410
return visits;
411
},
412
});
413
414
var PlacesUtils = {
415
// Place entries that are containers, e.g. bookmark folders or queries.
416
TYPE_X_MOZ_PLACE_CONTAINER: "text/x-moz-place-container",
417
// Place entries that are bookmark separators.
418
TYPE_X_MOZ_PLACE_SEPARATOR: "text/x-moz-place-separator",
419
// Place entries that are not containers or separators
420
TYPE_X_MOZ_PLACE: "text/x-moz-place",
421
// Place entries in shortcut url format (url\ntitle)
422
TYPE_X_MOZ_URL: "text/x-moz-url",
423
// Place entries formatted as HTML anchors
424
TYPE_HTML: "text/html",
425
// Place entries as raw URL text
426
TYPE_UNICODE: "text/unicode",
427
// Used to track the action that populated the clipboard.
428
TYPE_X_MOZ_PLACE_ACTION: "text/x-moz-place-action",
429
430
// Deprecated: Remaining only for supporting migration of old livemarks.
431
LMANNO_FEEDURI: "livemark/feedURI",
432
LMANNO_SITEURI: "livemark/siteURI",
433
CHARSET_ANNO: "URIProperties/characterSet",
434
// Deprecated: This is only used for supporting import from older datasets.
435
MOBILE_ROOT_ANNO: "mobile/bookmarksRoot",
436
437
TOPIC_SHUTDOWN: "places-shutdown",
438
TOPIC_INIT_COMPLETE: "places-init-complete",
439
TOPIC_DATABASE_LOCKED: "places-database-locked",
440
TOPIC_EXPIRATION_FINISHED: "places-expiration-finished",
441
TOPIC_FAVICONS_EXPIRED: "places-favicons-expired",
442
TOPIC_VACUUM_STARTING: "places-vacuum-starting",
443
TOPIC_BOOKMARKS_RESTORE_BEGIN: "bookmarks-restore-begin",
444
TOPIC_BOOKMARKS_RESTORE_SUCCESS: "bookmarks-restore-success",
445
TOPIC_BOOKMARKS_RESTORE_FAILED: "bookmarks-restore-failed",
446
447
observers: PlacesObservers,
448
449
/**
450
* GUIDs associated with virtual queries that are used for displaying the
451
* top-level folders in the left pane.
452
*/
453
virtualAllBookmarksGuid: "allbms_____v",
454
virtualHistoryGuid: "history____v",
455
virtualDownloadsGuid: "downloads__v",
456
virtualTagsGuid: "tags_______v",
457
458
/**
459
* Checks if a guid is a virtual left-pane root.
460
*
461
* @param {String} guid The guid of the item to look for.
462
* @returns {Boolean} true if guid is a virtual root, false otherwise.
463
*/
464
isVirtualLeftPaneItem(guid) {
465
return (
466
guid == PlacesUtils.virtualAllBookmarksGuid ||
467
guid == PlacesUtils.virtualHistoryGuid ||
468
guid == PlacesUtils.virtualDownloadsGuid ||
469
guid == PlacesUtils.virtualTagsGuid
470
);
471
},
472
473
asContainer: aNode => asContainer(aNode),
474
asQuery: aNode => asQuery(aNode),
475
476
endl: NEWLINE,
477
478
/**
479
* Is a string a valid GUID?
480
*
481
* @param guid: (String)
482
* @return (Boolean)
483
*/
484
isValidGuid(guid) {
485
return typeof guid == "string" && guid && /^[a-zA-Z0-9\-_]{12}$/.test(guid);
486
},
487
488
/**
489
* Is a string a valid GUID prefix?
490
*
491
* @param guidPrefix: (String)
492
* @return (Boolean)
493
*/
494
isValidGuidPrefix(guidPrefix) {
495
return (
496
typeof guidPrefix == "string" &&
497
guidPrefix &&
498
/^[a-zA-Z0-9\-_]{1,11}$/.test(guidPrefix)
499
);
500
},
501
502
/**
503
* Generates a random GUID and replace its beginning with the given
504
* prefix. We do this instead of just prepending the prefix to keep
505
* the correct character length.
506
*
507
* @param prefix: (String)
508
* @return (String)
509
*/
510
generateGuidWithPrefix(prefix) {
511
return prefix + this.history.makeGuid().substring(prefix.length);
512
},
513
514
/**
515
* Converts a string or n URL object to an nsIURI.
516
*
517
* @param url (URL) or (String)
518
* the URL to convert.
519
* @return nsIURI for the given URL.
520
*/
521
toURI(url) {
522
url = url instanceof URL ? url.href : url;
523
524
return NetUtil.newURI(url);
525
},
526
527
/**
528
* Convert a Date object to a PRTime (microseconds).
529
*
530
* @param date
531
* the Date object to convert.
532
* @return microseconds from the epoch.
533
*/
534
toPRTime(date) {
535
if (typeof date != "number" && date.constructor.name != "Date") {
536
throw new Error("Invalid value passed to toPRTime");
537
}
538
return date * 1000;
539
},
540
541
/**
542
* Convert a PRTime to a Date object.
543
*
544
* @param time
545
* microseconds from the epoch.
546
* @return a Date object.
547
*/
548
toDate(time) {
549
if (typeof time != "number") {
550
throw new Error("Invalid value passed to toDate");
551
}
552
return new Date(parseInt(time / 1000));
553
},
554
555
/**
556
* Wraps a string in a nsISupportsString wrapper.
557
* @param aString
558
* The string to wrap.
559
* @returns A nsISupportsString object containing a string.
560
*/
561
toISupportsString: function PU_toISupportsString(aString) {
562
let s = Cc["@mozilla.org/supports-string;1"].createInstance(
563
Ci.nsISupportsString
564
);
565
s.data = aString;
566
return s;
567
},
568
569
getFormattedString: function PU_getFormattedString(key, params) {
570
return bundle.formatStringFromName(key, params);
571
},
572
573
getString: function PU_getString(key) {
574
return bundle.GetStringFromName(key);
575
},
576
577
/**
578
* Parses a moz-action URL and returns its parts.
579
*
580
* @param url A moz-action URI.
581
* @note URL is in the format moz-action:ACTION,JSON_ENCODED_PARAMS
582
*/
583
parseActionUrl(url) {
584
if (url instanceof Ci.nsIURI) {
585
url = url.spec;
586
} else if (url instanceof URL) {
587
url = url.href;
588
}
589
// Faster bailout.
590
if (!url.startsWith("moz-action:")) {
591
return null;
592
}
593
594
try {
595
let [, type, params] = url.match(MOZ_ACTION_REGEX);
596
let action = {
597
type,
598
params: JSON.parse(params),
599
};
600
for (let key in action.params) {
601
action.params[key] = decodeURIComponent(action.params[key]);
602
}
603
return action;
604
} catch (ex) {
605
Cu.reportError(`Invalid action url "${url}"`);
606
return null;
607
}
608
},
609
610
/**
611
* Parses matchBuckets strings (for example, "suggestion:4,general:Infinity")
612
* like those used in the browser.urlbar.matchBuckets preference.
613
*
614
* @param str
615
* A matchBuckets string.
616
* @returns An array of the form: [
617
* [bucketName_0, bucketPriority_0],
618
* [bucketName_1, bucketPriority_1],
619
* ...
620
* [bucketName_n, bucketPriority_n]
621
* ]
622
*/
623
convertMatchBucketsStringToArray(str) {
624
return str.split(",").map(v => {
625
let bucket = v.split(":");
626
return [bucket[0].trim().toLowerCase(), Number(bucket[1])];
627
});
628
},
629
630
/**
631
* Determines if a folder is generated from a query.
632
* @param aNode a result true.
633
* @returns true if the node is a folder generated from a query.
634
*/
635
isQueryGeneratedFolder(node) {
636
if (!node.parent) {
637
return false;
638
}
639
return this.nodeIsFolder(node) && this.nodeIsQuery(node.parent);
640
},
641
642
/**
643
* Determines whether or not a ResultNode is a Bookmark folder.
644
* @param aNode
645
* A result node
646
* @returns true if the node is a Bookmark folder, false otherwise
647
*/
648
nodeIsFolder: function PU_nodeIsFolder(aNode) {
649
return (
650
aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER ||
651
aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT
652
);
653
},
654
655
/**
656
* Determines whether or not a ResultNode represents a bookmarked URI.
657
* @param aNode
658
* A result node
659
* @returns true if the node represents a bookmarked URI, false otherwise
660
*/
661
nodeIsBookmark: function PU_nodeIsBookmark(aNode) {
662
return (
663
aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI &&
664
aNode.itemId != -1
665
);
666
},
667
668
/**
669
* Determines whether or not a ResultNode is a Bookmark separator.
670
* @param aNode
671
* A result node
672
* @returns true if the node is a Bookmark separator, false otherwise
673
*/
674
nodeIsSeparator: function PU_nodeIsSeparator(aNode) {
675
return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR;
676
},
677
678
/**
679
* Determines whether or not a ResultNode is a URL item.
680
* @param aNode
681
* A result node
682
* @returns true if the node is a URL item, false otherwise
683
*/
684
nodeIsURI: function PU_nodeIsURI(aNode) {
685
return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
686
},
687
688
/**
689
* Determines whether or not a ResultNode is a Query item.
690
* @param aNode
691
* A result node
692
* @returns true if the node is a Query item, false otherwise
693
*/
694
nodeIsQuery: function PU_nodeIsQuery(aNode) {
695
return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY;
696
},
697
698
/**
699
* Generator for a node's ancestors.
700
* @param aNode
701
* A result node
702
*/
703
nodeAncestors: function* PU_nodeAncestors(aNode) {
704
let node = aNode.parent;
705
while (node) {
706
yield node;
707
node = node.parent;
708
}
709
},
710
711
/**
712
* Checks validity of an object, filling up default values for optional
713
* properties.
714
*
715
* @param {string} name
716
* The operation name. This is included in the error message if
717
* validation fails.
718
* @param validators (object)
719
* An object containing input validators. Keys should be field names;
720
* values should be validation functions.
721
* @param props (object)
722
* The object to validate.
723
* @param behavior (object) [optional]
724
* Object defining special behavior for some of the properties.
725
* The following behaviors may be optionally set:
726
* - required: this property is required.
727
* - replaceWith: this property will be overwritten with the value
728
* provided
729
* - requiredIf: if the provided condition is satisfied, then this
730
* property is required.
731
* - validIf: if the provided condition is not satisfied, then this
732
* property is invalid.
733
* - defaultValue: an undefined property should default to this value.
734
* - fixup: a function invoked when validation fails, takes the input
735
* object as argument and must fix the property.
736
*
737
* @return a validated and normalized item.
738
* @throws if the object contains invalid data.
739
* @note any unknown properties are pass-through.
740
*/
741
validateItemProperties(name, validators, props, behavior = {}) {
742
if (typeof props != "object" || !props) {
743
throw new Error(`${name}: Input should be a valid object`);
744
}
745
// Make a shallow copy of `props` to avoid mutating the original object
746
// when filling in defaults.
747
let input = Object.assign({}, props);
748
let normalizedInput = {};
749
let required = new Set();
750
for (let prop in behavior) {
751
if (
752
behavior[prop].hasOwnProperty("required") &&
753
behavior[prop].required
754
) {
755
required.add(prop);
756
}
757
if (
758
behavior[prop].hasOwnProperty("requiredIf") &&
759
behavior[prop].requiredIf(input)
760
) {
761
required.add(prop);
762
}
763
if (
764
behavior[prop].hasOwnProperty("validIf") &&
765
input[prop] !== undefined &&
766
!behavior[prop].validIf(input)
767
) {
768
if (behavior[prop].hasOwnProperty("fixup")) {
769
behavior[prop].fixup(input);
770
} else {
771
throw new Error(
772
`${name}: Invalid value for property '${prop}': ${JSON.stringify(
773
input[prop]
774
)}`
775
);
776
}
777
}
778
if (
779
behavior[prop].hasOwnProperty("defaultValue") &&
780
input[prop] === undefined
781
) {
782
input[prop] = behavior[prop].defaultValue;
783
}
784
if (behavior[prop].hasOwnProperty("replaceWith")) {
785
input[prop] = behavior[prop].replaceWith;
786
}
787
}
788
789
for (let prop in input) {
790
if (required.has(prop)) {
791
required.delete(prop);
792
} else if (input[prop] === undefined) {
793
// Skip undefined properties that are not required.
794
continue;
795
}
796
if (validators.hasOwnProperty(prop)) {
797
try {
798
normalizedInput[prop] = validators[prop](input[prop], input);
799
} catch (ex) {
800
if (
801
behavior.hasOwnProperty(prop) &&
802
behavior[prop].hasOwnProperty("fixup")
803
) {
804
behavior[prop].fixup(input);
805
normalizedInput[prop] = input[prop];
806
} else {
807
throw new Error(
808
`${name}: Invalid value for property '${prop}': ${JSON.stringify(
809
input[prop]
810
)}`
811
);
812
}
813
}
814
}
815
}
816
if (required.size > 0) {
817
throw new Error(
818
`${name}: The following properties were expected: ${[...required].join(
819
", "
820
)}`
821
);
822
}
823
return normalizedInput;
824
},
825
826
BOOKMARK_VALIDATORS,
827
SYNC_BOOKMARK_VALIDATORS,
828
SYNC_CHANGE_RECORD_VALIDATORS,
829
830
QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver]),
831
832
_shutdownFunctions: [],
833
registerShutdownFunction: function PU_registerShutdownFunction(aFunc) {
834
// If this is the first registered function, add the shutdown observer.
835
if (!this._shutdownFunctions.length) {
836
Services.obs.addObserver(this, this.TOPIC_SHUTDOWN);
837
}
838
this._shutdownFunctions.push(aFunc);
839
},
840
841
// nsIObserver
842
observe: function PU_observe(aSubject, aTopic, aData) {
843
switch (aTopic) {
844
case this.TOPIC_SHUTDOWN:
845
Services.obs.removeObserver(this, this.TOPIC_SHUTDOWN);
846
while (this._shutdownFunctions.length) {
847
this._shutdownFunctions.shift().apply(this);
848
}
849
break;
850
}
851
},
852
853
/**
854
* Determines whether or not a ResultNode is a host container.
855
* @param aNode
856
* A result node
857
* @returns true if the node is a host container, false otherwise
858
*/
859
nodeIsHost: function PU_nodeIsHost(aNode) {
860
return (
861
aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
862
aNode.parent &&
863
asQuery(aNode.parent).queryOptions.resultType ==
864
Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY
865
);
866
},
867
868
/**
869
* Determines whether or not a ResultNode is a day container.
870
* @param node
871
* A NavHistoryResultNode
872
* @returns true if the node is a day container, false otherwise
873
*/
874
nodeIsDay: function PU_nodeIsDay(aNode) {
875
var resultType;
876
return (
877
aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
878
aNode.parent &&
879
((resultType = asQuery(aNode.parent).queryOptions.resultType) ==
880
Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
881
resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY)
882
);
883
},
884
885
/**
886
* Determines whether or not a result-node is a tag container.
887
* @param aNode
888
* A result-node
889
* @returns true if the node is a tag container, false otherwise
890
*/
891
nodeIsTagQuery: function PU_nodeIsTagQuery(aNode) {
892
if (aNode.type != Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) {
893
return false;
894
}
895
// Direct child of RESULTS_AS_TAGS_ROOT.
896
let parent = aNode.parent;
897
if (
898
parent &&
899
PlacesUtils.asQuery(parent).queryOptions.resultType ==
900
Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAGS_ROOT
901
) {
902
return true;
903
}
904
// We must also support the right pane of the Library, when the tag query
905
// is the root node. Unfortunately this is also valid for any tag query
906
// selected in the left pane that is not a direct child of RESULTS_AS_TAGS_ROOT.
907
if (
908
!parent &&
909
aNode == aNode.parentResult.root &&
910
PlacesUtils.asQuery(aNode).query.tags.length == 1
911
) {
912
return true;
913
}
914
return false;
915
},
916
917
/**
918
* Determines whether or not a ResultNode is a container.
919
* @param aNode
920
* A result node
921
* @returns true if the node is a container item, false otherwise
922
*/
923
containerTypes: [
924
Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
925
Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT,
926
Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY,
927
],
928
nodeIsContainer: function PU_nodeIsContainer(aNode) {
929
return this.containerTypes.includes(aNode.type);
930
},
931
932
/**
933
* Determines whether or not a ResultNode is an history related container.
934
* @param node
935
* A result node
936
* @returns true if the node is an history related container, false otherwise
937
*/
938
nodeIsHistoryContainer: function PU_nodeIsHistoryContainer(aNode) {
939
var resultType;
940
return (
941
this.nodeIsQuery(aNode) &&
942
((resultType = asQuery(aNode).queryOptions.resultType) ==
943
Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY ||
944
resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
945
resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY ||
946
this.nodeIsDay(aNode) ||
947
this.nodeIsHost(aNode))
948
);
949
},
950
951
/**
952
* Gets the concrete item-id for the given node. Generally, this is just
953
* node.itemId, but for folder-shortcuts that's node.folderItemId.
954
*/
955
getConcreteItemId: function PU_getConcreteItemId(aNode) {
956
return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT
957
? asQuery(aNode).folderItemId
958
: aNode.itemId;
959
},
960
961
/**
962
* Gets the concrete item-guid for the given node. For everything but folder
963
* shortcuts, this is just node.bookmarkGuid. For folder shortcuts, this is
964
* node.targetFolderGuid (see nsINavHistoryService.idl for the semantics).
965
*
966
* @param aNode
967
* a result node.
968
* @return the concrete item-guid for aNode.
969
*/
970
getConcreteItemGuid(aNode) {
971
if (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT) {
972
return asQuery(aNode).targetFolderGuid;
973
}
974
return aNode.bookmarkGuid;
975
},
976
977
/**
978
* Reverse a host based on the moz_places algorithm, that is reverse the host
979
* string and add a trailing period. For example "google.com" becomes
980
* "moc.elgoog.".
981
*
982
* @param url
983
* the URL to generate a rev host for.
984
* @return the reversed host string.
985
*/
986
getReversedHost(url) {
987
return (
988
url.host
989
.split("")
990
.reverse()
991
.join("") + "."
992
);
993
},
994
995
/**
996
* String-wraps a result node according to the rules of the specified
997
* content type for copy or move operations.
998
*
999
* @param aNode
1000
* The Result node to wrap (serialize)
1001
* @param aType
1002
* The content type to serialize as
1003
* @return A string serialization of the node
1004
*/
1005
wrapNode(aNode, aType) {
1006
// when wrapping a node, we want all the items, even if the original
1007
// query options are excluding them.
1008
// This can happen when copying from the left hand pane of the bookmarks
1009
// organizer.
1010
// @return [node, shouldClose]
1011
function gatherDataFromNode(node, gatherDataFunc) {
1012
if (
1013
PlacesUtils.nodeIsFolder(node) &&
1014
node.type != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT &&
1015
asQuery(node).queryOptions.excludeItems
1016
) {
1017
let folderRoot = PlacesUtils.getFolderContents(
1018
node.bookmarkGuid,
1019
false,
1020
true
1021
).root;
1022
try {
1023
return gatherDataFunc(folderRoot);
1024
} finally {
1025
folderRoot.containerOpen = false;
1026
}
1027
}
1028
// If we didn't create our own query, do not alter the node's state.
1029
return gatherDataFunc(node);
1030
}
1031
1032
function gatherDataHtml(node) {
1033
let htmlEscape = s =>
1034
s
1035
.replace(/&/g, "&amp;")
1036
.replace(/>/g, "&gt;")
1037
.replace(/</g, "&lt;")
1038
.replace(/"/g, "&quot;")
1039
.replace(/'/g, "&apos;");
1040
1041
// escape out potential HTML in the title
1042
let escapedTitle = node.title ? htmlEscape(node.title) : "";
1043
1044
if (PlacesUtils.nodeIsContainer(node)) {
1045
asContainer(node);
1046
let wasOpen = node.containerOpen;
1047
if (!wasOpen) {
1048
node.containerOpen = true;
1049
}
1050
1051
let childString = "<DL><DT>" + escapedTitle + "</DT>" + NEWLINE;
1052
let cc = node.childCount;
1053
for (let i = 0; i < cc; ++i) {
1054
childString +=
1055
"<DD>" +
1056
NEWLINE +
1057
gatherDataHtml(node.getChild(i)) +
1058
"</DD>" +
1059
NEWLINE;
1060
}
1061
node.containerOpen = wasOpen;
1062
return childString + "</DL>" + NEWLINE;
1063
}
1064
if (PlacesUtils.nodeIsURI(node)) {
1065
return `<A HREF="${node.uri}">${escapedTitle}</A>${NEWLINE}`;
1066
}
1067
if (PlacesUtils.nodeIsSeparator(node)) {
1068
return "<HR>" + NEWLINE;
1069
}
1070
return "";
1071
}
1072
1073
function gatherDataText(node) {
1074
if (PlacesUtils.nodeIsContainer(node)) {
1075
asContainer(node);
1076
let wasOpen = node.containerOpen;
1077
if (!wasOpen) {
1078
node.containerOpen = true;
1079
}
1080
1081
let childString = node.title + NEWLINE;
1082
let cc = node.childCount;
1083
for (let i = 0; i < cc; ++i) {
1084
let child = node.getChild(i);
1085
let suffix = i < cc - 1 ? NEWLINE : "";
1086
childString += gatherDataText(child) + suffix;
1087
}
1088
node.containerOpen = wasOpen;
1089
return childString;
1090
}
1091
if (PlacesUtils.nodeIsURI(node)) {
1092
return node.uri;
1093
}
1094
if (PlacesUtils.nodeIsSeparator(node)) {
1095
return "--------------------";
1096
}
1097
return "";
1098
}
1099
1100
switch (aType) {
1101
case this.TYPE_X_MOZ_PLACE:
1102
case this.TYPE_X_MOZ_PLACE_SEPARATOR:
1103
case this.TYPE_X_MOZ_PLACE_CONTAINER: {
1104
// Serialize the node to JSON.
1105
return serializeNode(aNode);
1106
}
1107
case this.TYPE_X_MOZ_URL: {
1108
if (PlacesUtils.nodeIsURI(aNode)) {
1109
return aNode.uri + NEWLINE + aNode.title;
1110
}
1111
if (PlacesUtils.nodeIsContainer(aNode)) {
1112
return PlacesUtils.getURLsForContainerNode(aNode)
1113
.map(item => item.uri + "\n" + item.title)
1114
.join("\n");
1115
}
1116
return "";
1117
}
1118
case this.TYPE_HTML: {
1119
return gatherDataFromNode(aNode, gatherDataHtml);
1120
}
1121
}
1122
1123
// Otherwise, we wrap as TYPE_UNICODE.
1124
return gatherDataFromNode(aNode, gatherDataText);
1125
},
1126
1127
/**
1128
* Unwraps data from the Clipboard or the current Drag Session.
1129
* @param blob
1130
* A blob (string) of data, in some format we potentially know how
1131
* to parse.
1132
* @param type
1133
* The content type of the blob.
1134
* @returns An array of objects representing each item contained by the source.
1135
* @throws if the blob contains invalid data.
1136
*/
1137
unwrapNodes: function PU_unwrapNodes(blob, type) {
1138
// We split on "\n" because the transferable system converts "\r\n" to "\n"
1139
var nodes = [];
1140
switch (type) {
1141
case this.TYPE_X_MOZ_PLACE:
1142
case this.TYPE_X_MOZ_PLACE_SEPARATOR:
1143
case this.TYPE_X_MOZ_PLACE_CONTAINER:
1144
nodes = JSON.parse("[" + blob + "]");
1145
break;
1146
case this.TYPE_X_MOZ_URL: {
1147
let parts = blob.split("\n");
1148
// data in this type has 2 parts per entry, so if there are fewer
1149
// than 2 parts left, the blob is malformed and we should stop
1150
// but drag and drop of files from the shell has parts.length = 1
1151
if (parts.length != 1 && parts.length % 2) {
1152
break;
1153
}
1154
for (let i = 0; i < parts.length; i = i + 2) {
1155
let uriString = parts[i];
1156
let titleString = "";
1157
if (parts.length > i + 1) {
1158
titleString = parts[i + 1];
1159
} else {
1160
// for drag and drop of files, try to use the leafName as title
1161
try {
1162
titleString = Services.io
1163
.newURI(uriString)
1164
.QueryInterface(Ci.nsIURL).fileName;
1165
} catch (ex) {}
1166
}
1167
// note: Services.io.newURI() will throw if uriString is not a valid URI
1168
let uri = Services.io.newURI(uriString);
1169
if (Services.io.newURI(uriString) && uri.scheme != "place") {
1170
nodes.push({
1171
uri: uriString,
1172
title: titleString ? titleString : uriString,
1173
type: this.TYPE_X_MOZ_URL,
1174
});
1175
}
1176
}
1177
break;
1178
}
1179
case this.TYPE_UNICODE: {
1180
let parts = blob.split("\n");
1181
for (let i = 0; i < parts.length; i++) {
1182
let uriString = parts[i];
1183
// text/uri-list is converted to TYPE_UNICODE but it could contain
1184
// comments line prepended by #, we should skip them, as well as
1185
// empty uris.
1186
if (uriString.substr(0, 1) == "\x23" || uriString == "") {
1187
continue;
1188
}
1189
// note: Services.io.newURI) will throw if uriString is not a valid URI
1190
let uri = Services.io.newURI(uriString);
1191
if (uri.scheme != "place") {
1192
nodes.push({
1193
uri: uriString,
1194
title: uriString,
1195
type: this.TYPE_X_MOZ_URL,
1196
});
1197
}
1198
}
1199
break;
1200
}
1201
default:
1202
throw Cr.NS_ERROR_INVALID_ARG;
1203
}
1204
return nodes;
1205
},
1206
1207
/**
1208
* Validate an input PageInfo object, returning a valid PageInfo object.
1209
*
1210
* @param pageInfo: (PageInfo)
1211
* @return (PageInfo)
1212
*/
1213
validatePageInfo(pageInfo, validateVisits = true) {
1214
return this.validateItemProperties(
1215
"PageInfo",
1216
PAGEINFO_VALIDATORS,
1217
pageInfo,
1218
{
1219
url: { requiredIf: b => !b.guid },
1220
guid: { requiredIf: b => !b.url },
1221
visits: { requiredIf: b => validateVisits },
1222
}
1223
);
1224
},
1225
/**
1226
* Normalize a key to either a string (if it is a valid GUID) or an
1227
* instance of `URL` (if it is a `URL`, `nsIURI`, or a string
1228
* representing a valid url).
1229
*
1230
* @throws (TypeError)
1231
* If the key is neither a valid guid nor a valid url.
1232
*/
1233
normalizeToURLOrGUID(key) {
1234
if (typeof key === "string") {
1235
// A string may be a URL or a guid
1236
if (this.isValidGuid(key)) {
1237
return key;
1238
}
1239
return new URL(key);
1240
}
1241
if (key instanceof URL) {
1242
return key;
1243
}
1244
if (key instanceof Ci.nsIURI) {
1245
return new URL(key.spec);
1246
}
1247
throw new TypeError("Invalid url or guid: " + key);
1248
},
1249
1250
/**
1251
* Generates a nsINavHistoryResult for the contents of a folder.
1252
* @param aFolderGuid
1253
* The folder to open
1254
* @param [optional] excludeItems
1255
* True to hide all items (individual bookmarks). This is used on
1256
* the left places pane so you just get a folder hierarchy.
1257
* @param [optional] expandQueries
1258
* True to make query items expand as new containers. For managing,
1259
* you want this to be false, for menus and such, you want this to
1260
* be true.
1261
* @returns A nsINavHistoryResult containing the contents of the
1262
* folder. The result.root is guaranteed to be open.
1263
*/
1264
getFolderContents(aFolderGuid, aExcludeItems, aExpandQueries) {
1265
if (!this.isValidGuid(aFolderGuid)) {
1266
throw new Error("aFolderGuid should be a valid GUID.");
1267
}
1268
var query = this.history.getNewQuery();
1269
query.setParents([aFolderGuid]);
1270
var options = this.history.getNewQueryOptions();
1271
options.excludeItems = aExcludeItems;
1272
options.expandQueries = aExpandQueries;
1273
1274
var result = this.history.executeQuery(query, options);
1275
result.root.containerOpen = true;
1276
return result;
1277
},
1278
1279
// Identifier getters for special folders.
1280
// You should use these everywhere PlacesUtils is available to avoid XPCOM
1281
// traversal just to get roots' ids.
1282
get placesRootId() {
1283
delete this.placesRootId;
1284
return (this.placesRootId = this.bookmarks.placesRoot);
1285
},
1286
1287
get bookmarksMenuFolderId() {
1288
delete this.bookmarksMenuFolderId;
1289
return (this.bookmarksMenuFolderId = this.bookmarks.bookmarksMenuFolder);
1290
},
1291
1292
get toolbarFolderId() {
1293
delete this.toolbarFolderId;
1294
return (this.toolbarFolderId = this.bookmarks.toolbarFolder);
1295
},
1296
1297
get tagsFolderId() {
1298
delete this.tagsFolderId;
1299
return (this.tagsFolderId = this.bookmarks.tagsFolder);
1300
},
1301
1302
/**
1303
* Checks if item is a root.
1304
*
1305
* @param {String} guid The guid of the item to look for.
1306
* @returns {Boolean} true if guid is a root, false otherwise.
1307
*/
1308
isRootItem(guid) {
1309
return (
1310
guid == PlacesUtils.bookmarks.menuGuid ||
1311
guid == PlacesUtils.bookmarks.toolbarGuid ||
1312
guid == PlacesUtils.bookmarks.unfiledGuid ||
1313
guid == PlacesUtils.bookmarks.tagsGuid ||
1314
guid == PlacesUtils.bookmarks.rootGuid ||
1315
guid == PlacesUtils.bookmarks.mobileGuid
1316
);
1317
},
1318
1319
/**
1320
* Returns a nsNavHistoryContainerResultNode with forced excludeItems and
1321
* expandQueries.
1322
* @param aNode
1323
* The node to convert
1324
* @param [optional] excludeItems
1325
* True to hide all items (individual bookmarks). This is used on
1326
* the left places pane so you just get a folder hierarchy.
1327
* @param [optional] expandQueries
1328
* True to make query items expand as new containers. For managing,
1329
* you want this to be false, for menus and such, you want this to
1330
* be true.
1331
* @returns A nsINavHistoryContainerResultNode containing the unfiltered
1332
* contents of the container.
1333
* @note The returned container node could be open or closed, we don't
1334
* guarantee its status.
1335
*/
1336
getContainerNodeWithOptions: function PU_getContainerNodeWithOptions(
1337
aNode,
1338
aExcludeItems,
1339
aExpandQueries
1340
) {
1341
if (!this.nodeIsContainer(aNode)) {
1342
throw Cr.NS_ERROR_INVALID_ARG;
1343
}
1344
1345
// excludeItems is inherited by child containers in an excludeItems view.
1346
var excludeItems =
1347
asQuery(aNode).queryOptions.excludeItems ||
1348
asQuery(aNode.parentResult.root).queryOptions.excludeItems;
1349
// expandQueries is inherited by child containers in an expandQueries view.
1350
var expandQueries =
1351
asQuery(aNode).queryOptions.expandQueries &&
1352
asQuery(aNode.parentResult.root).queryOptions.expandQueries;
1353
1354
// If our options are exactly what we expect, directly return the node.
1355
if (excludeItems == aExcludeItems && expandQueries == aExpandQueries) {
1356
return aNode;
1357
}
1358
1359
// Otherwise, get contents manually.
1360
var query = {},
1361
options = {};
1362
this.history.queryStringToQuery(aNode.uri, query, options);
1363
options.value.excludeItems = aExcludeItems;
1364
options.value.expandQueries = aExpandQueries;
1365
return this.history.executeQuery(query.value, options.value).root;
1366
},
1367
1368
/**
1369
* Returns true if a container has uri nodes in its first level.
1370
* Has better performance than (getURLsForContainerNode(node).length > 0).
1371
* @param aNode
1372
* The container node to search through.
1373
* @returns true if the node contains uri nodes, false otherwise.
1374
*/
1375
hasChildURIs: function PU_hasChildURIs(aNode) {
1376
if (!this.nodeIsContainer(aNode)) {
1377
return false;
1378
}
1379
1380
let root = this.getContainerNodeWithOptions(aNode, false, true);
1381
let result = root.parentResult;
1382
let didSuppressNotifications = false;
1383
let wasOpen = root.containerOpen;
1384
if (!wasOpen) {
1385
didSuppressNotifications = result.suppressNotifications;
1386
if (!didSuppressNotifications) {
1387
result.suppressNotifications = true;
1388
}
1389
1390
root.containerOpen = true;
1391
}
1392
1393
let found = false;
1394
for (let i = 0; i < root.childCount && !found; i++) {
1395
let child = root.getChild(i);
1396
if (this.nodeIsURI(child)) {
1397
found = true;
1398
}
1399
}
1400
1401
if (!wasOpen) {
1402
root.containerOpen = false;
1403
if (!didSuppressNotifications) {
1404
result.suppressNotifications = false;
1405
}
1406
}
1407
return found;
1408
},
1409
1410
/**
1411
* Returns an array containing all the uris in the first level of the
1412
* passed in container.
1413
* If you only need to know if the node contains uris, use hasChildURIs.
1414
* @param aNode
1415
* The container node to search through
1416
* @returns array of uris in the first level of the container.
1417
*/
1418
getURLsForContainerNode: function PU_getURLsForContainerNode(aNode) {
1419
let urls = [];
1420
if (!this.nodeIsContainer(aNode)) {
1421
return urls;
1422
}
1423
1424
let root = this.getContainerNodeWithOptions(aNode, false, true);
1425
let result = root.parentResult;
1426
let wasOpen = root.containerOpen;
1427
let didSuppressNotifications = false;
1428
if (!wasOpen) {
1429
didSuppressNotifications = result.suppressNotifications;
1430
if (!didSuppressNotifications) {
1431
result.suppressNotifications = true;
1432
}
1433
1434
root.containerOpen = true;
1435
}
1436
1437
for (let i = 0; i < root.childCount; ++i) {
1438
let child = root.getChild(i);
1439
if (this.nodeIsURI(child)) {
1440
urls.push({
1441
uri: child.uri,
1442
isBookmark: this.nodeIsBookmark(child),
1443
title: child.title,
1444
});
1445
}
1446
}
1447
1448
if (!wasOpen) {
1449
root.containerOpen = false;
1450
if (!didSuppressNotifications) {
1451
result.suppressNotifications = false;
1452
}
1453
}
1454
return urls;
1455
},
1456
1457
/**
1458
* Gets a shared Sqlite.jsm readonly connection to the Places database,
1459
* usable only for SELECT queries.
1460
*
1461
* This is intended to be used mostly internally, components outside of
1462
* Places should, when possible, use API calls and file bugs to get proper
1463
* APIs, where they are missing.
1464
* Keep in mind the Places DB schema is by no means frozen or even stable.
1465
* Your custom queries can - and will - break overtime.
1466
*
1467
* Example:
1468
* let db = await PlacesUtils.promiseDBConnection();
1469
* let rows = await db.executeCached(sql, params);
1470
*/
1471
promiseDBConnection: () => gAsyncDBConnPromised,
1472
1473
/**
1474
* This is pretty much the same as promiseDBConnection, but with a larger
1475
* page cache, useful for consumers doing large table scans, like the urlbar.
1476
* @see promiseDBConnection
1477
*/
1478
promiseLargeCacheDBConnection: () => gAsyncDBLargeCacheConnPromised,
1479
1480
/**
1481
* Returns a Sqlite.jsm wrapper for the main Places connection. Most callers
1482
* should prefer `withConnectionWrapper`, which ensures that all database
1483
* operations finish before the connection is closed.
1484
*/
1485
promiseUnsafeWritableDBConnection: () => gAsyncDBWrapperPromised,
1486
1487
/**
1488
* Performs a read/write operation on the Places database through a Sqlite.jsm
1489
* wrapped connection to the Places database.
1490
*
1491
* This is intended to be used only by Places itself, always use APIs if you
1492
* need to modify the Places database. Use promiseDBConnection if you need to
1493
* SELECT from the database and there's no covering API.
1494
* Keep in mind the Places DB schema is by no means frozen or even stable.
1495
* Your custom queries can - and will - break overtime.
1496
*
1497
* As all operations on the Places database are asynchronous, if shutdown
1498
* is initiated while an operation is pending, this could cause dataloss.
1499
* Using `withConnectionWrapper` ensures that shutdown waits until all
1500
* operations are complete before proceeding.
1501
*
1502
* Example:
1503
* await withConnectionWrapper("Bookmarks: Remove a bookmark", Task.async(function*(db) {
1504
* // Proceed with the db, asynchronously.
1505
* // Shutdown will not interrupt operations that take place here.
1506
* }));
1507
*
1508
* @param {string} name The name of the operation. Used for debugging, logging
1509
* and crash reporting.
1510
* @param {function(db)} task A function that takes as argument a Sqlite.jsm
1511
* connection and returns a Promise. Shutdown is guaranteed to not interrupt
1512
* execution of `task`.
1513
*/
1514
async withConnectionWrapper(name, task) {
1515
if (!name) {
1516
throw new TypeError("Expecting a user-readable name");
1517
}
1518
let db = await gAsyncDBWrapperPromised;
1519
return db.executeBeforeShutdown(name, task);
1520
},
1521
1522
/**
1523
* Gets favicon data for a given page url.
1524
*
1525
* @param aPageUrl url of the page to look favicon for.
1526
* @resolves to an object representing a favicon entry, having the following
1527
* properties: { uri, dataLen, data, mimeType }
1528
* @rejects JavaScript exception if the given url has no associated favicon.
1529
*/
1530
promiseFaviconData(aPageUrl) {
1531
return new Promise((resolve, reject) => {
1532
PlacesUtils.favicons.getFaviconDataForPage(
1533
NetUtil.newURI(aPageUrl),
1534
function(uri, dataLen, data, mimeType) {
1535
if (uri) {
1536
resolve({ uri, dataLen, data, mimeType });
1537
} else {
1538
reject();
1539
}
1540
}
1541
);
1542
});
1543
},
1544
1545
/**
1546
* Returns the passed URL with a #size ref for the specified size and
1547
* devicePixelRatio.
1548
*
1549
* @param window
1550
* The window where the icon will appear.
1551
* @param href
1552
* The string href we should add the ref to.
1553
* @param size
1554
* The target image size
1555
* @return The URL with the fragment at the end, in the same formar as input.
1556
*/
1557
urlWithSizeRef(window, href, size) {
1558
return (
1559
href +
1560
(href.includes("#") ? "&" : "#") +
1561
"size=" +
1562
Math.round(size) * window.devicePixelRatio
1563
);
1564
},
1565
1566
/**
1567
* Get the unique id for an item (a bookmark, a folder or a separator) given
1568
* its item id.
1569
*
1570
* @param aItemId
1571
* an item id
1572
* @return {Promise}
1573
* @resolves to the GUID.
1574
* @rejects if aItemId is invalid.
1575
*/
1576
promiseItemGuid(aItemId) {
1577
return GuidHelper.getItemGuid(aItemId);
1578
},
1579
1580
/**
1581
* Get the item id for an item (a bookmark, a folder or a separator) given
1582
* its unique id.
1583
*
1584
* @param aGuid
1585
* an item GUID
1586
* @return {Promise}
1587
* @resolves to the item id.
1588
* @rejects if there's no item for the given GUID.
1589
*/
1590
promiseItemId(aGuid) {
1591
return GuidHelper.getItemId(aGuid);
1592
},
1593
1594
/**
1595
* Get the item ids for multiple items (a bookmark, a folder or a separator)
1596
* given the unique ids for each item.
1597
*
1598
* @param {Array} aGuids An array of item GUIDs.
1599
* @return {Promise}
1600
* @resolves to a Map of item ids.
1601
* @rejects if not all of the GUIDs could be found.
1602
*/
1603
promiseManyItemIds(aGuids) {
1604
return GuidHelper.getManyItemIds(aGuids);
1605
},
1606
1607
/**
1608
* Invalidate the GUID cache for the given itemId.
1609
*
1610
* @param aItemId
1611
* an item id
1612
*/
1613
invalidateCachedGuidFor(aItemId) {
1614
GuidHelper.invalidateCacheForItemId(aItemId);
1615
},
1616
1617
/**
1618
* Invalidates the entire GUID cache.
1619
*/
1620
invalidateCachedGuids() {
1621
GuidHelper.invalidateCache();
1622
},
1623
1624
/**
1625
* Asynchronously retrieve a JS-object representation of a places bookmarks
1626
* item (a bookmark, a folder, or a separator) along with all of its
1627
* descendants.
1628
*
1629
* @param [optional] aItemGuid
1630
* the (topmost) item to be queried. If it's not passed, the places
1631
* root is queried: that is, you get a representation of the entire
1632
* bookmarks hierarchy.
1633
* @param [optional] aOptions
1634
* Options for customizing the query behavior, in the form of a JS
1635
* object with any of the following properties:
1636
* - excludeItemsCallback: a function for excluding items, along with
1637
* their descendants. Given an item object (that has everything set
1638
* apart its potential children data), it should return true if the
1639
* item should be excluded. Once an item is excluded, the function
1640
* isn't called for any of its descendants. This isn't called for
1641
* the root item.
1642
* WARNING: since the function may be called for each item, using
1643
* this option can slow down the process significantly if the
1644
* callback does anything that's not relatively trivial. It is
1645
* highly recommended to avoid any synchronous I/O or DB queries.
1646
* - includeItemIds: opt-in to include the deprecated id property.
1647
* Use it if you must. It'll be removed once the switch to GUIDs is
1648
* complete.
1649
*
1650
* @return {Promise}
1651
* @resolves to a JS object that represents either a single item or a
1652
* bookmarks tree. Each node in the tree has the following properties set:
1653
* - guid (string): the item's GUID (same as aItemGuid for the top item).
1654
* - [deprecated] id (number): the item's id. This is only if
1655
* aOptions.includeItemIds is set.
1656
* - type (string): the item's type. @see PlacesUtils.TYPE_X_*
1657
* - typeCode (number): the item's type in numeric format.
1658
* @see PlacesUtils.bookmarks.TYPE_*
1659
* - title (string): the item's title. If it has no title, this property
1660
* isn't set.
1661
* - dateAdded (number, microseconds from the epoch): the date-added value of
1662
* the item.
1663
* - lastModified (number, microseconds from the epoch): the last-modified
1664
* value of the item.
1665
* - index: the item's index under it's parent.
1666
*
1667
* The root object (i.e. the one for aItemGuid) also has the following
1668
* properties set:
1669
* - parentGuid (string): the GUID of the root's parent. This isn't set if
1670
* the root item is the places root.
1671
* - itemsCount (number, not enumerable): the number of items, including the
1672
* root item itself, which are represented in the resolved object.
1673
*
1674
* Bookmark items also have the following properties:
1675
* - uri (string): the item's url.
1676
* - tags (string): csv string of the bookmark's tags.
1677
* - charset (string): the last known charset of the bookmark.
1678
* - keyword (string): the bookmark's keyword (unset if none).
1679
* - postData (string): the bookmark's keyword postData (unset if none).
1680
* - iconuri (string): the bookmark's favicon url.
1681
* The last four properties are not set at all if they're irrelevant (e.g.
1682
* |charset| is not set if no charset was previously set for the bookmark
1683
* url).
1684
*
1685
* Folders may also have the following properties:
1686
* - children (array): the folder's children information, each of them
1687
* having the same set of properties as above.
1688
*
1689
* @rejects if the query failed for any reason.
1690
* @note if aItemGuid points to a non-existent item, the returned promise is
1691
* resolved to null.
1692
*/
1693
async promiseBookmarksTree(aItemGuid = "", aOptions = {}) {
1694
let createItemInfoObject = async function(aRow, aIncludeParentGuid) {
1695
let item = {};
1696
let copyProps = (...props) => {
1697
for (let prop of props) {
1698
let val = aRow.getResultByName(prop);
1699
if (val !== null) {
1700
item[prop] = val;
1701
}
1702
}
1703
};
1704
copyProps("guid", "title", "index", "dateAdded", "lastModified");
1705
if (aIncludeParentGuid) {
1706
copyProps("parentGuid");
1707
}
1708
1709
let itemId = aRow.getResultByName("id");
1710
if (aOptions.includeItemIds) {
1711
item.id = itemId;
1712
}
1713
1714
// Cache it for promiseItemId consumers regardless.
1715
GuidHelper.updateCache(itemId, item.guid);
1716
1717
let type = aRow.getResultByName("type");
1718
item.typeCode = type;
1719
if (type == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
1720
copyProps("charset", "tags", "iconuri");
1721
}
1722
1723
switch (type) {
1724
case PlacesUtils.bookmarks.TYPE_BOOKMARK:
1725
item.type = PlacesUtils.TYPE_X_MOZ_PLACE;
1726
// If this throws due to an invalid url, the item will be skipped.
1727
try {
1728
item.uri = NetUtil.newURI(aRow.getResultByName("url")).spec;
1729
} catch (ex) {
1730
let error = new Error("Invalid bookmark URL");
1731
error.becauseInvalidURL = true;
1732
throw error;
1733
}
1734
// Keywords are cached, so this should be decently fast.
1735
let entry = await PlacesUtils.keywords.fetch({ url: item.uri });
1736
if (entry) {
1737
item.keyword = entry.keyword;
1738
item.postData = entry.postData;
1739
}
1740
break;
1741
case PlacesUtils.bookmarks.TYPE_FOLDER:
1742
item.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
1743
// Mark root folders.
1744
if (item.guid == PlacesUtils.bookmarks.rootGuid) {
1745
item.root = "placesRoot";
1746
} else if (item.guid == PlacesUtils.bookmarks.menuGuid) {
1747
item.root = "bookmarksMenuFolder";
1748
} else if (item.guid == PlacesUtils.bookmarks.unfiledGuid) {
1749
item.root = "unfiledBookmarksFolder";
1750
} else if (item.guid == PlacesUtils.bookmarks.toolbarGuid) {
1751
item.root = "toolbarFolder";
1752
} else if (item.guid == PlacesUtils.bookmarks.mobileGuid) {
1753
item.root = "mobileFolder";
1754
}
1755
break;
1756
case PlacesUtils.bookmarks.TYPE_SEPARATOR:
1757
item.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
1758
break;
1759
default:
1760
Cu.reportError(`Unexpected bookmark type ${type}`);
1761
break;
1762
}
1763
return item;
1764
};
1765
1766
const QUERY_STR = `/* do not warn (bug no): cannot use an index */
1767
WITH RECURSIVE
1768
descendants(fk, level, type, id, guid, parent, parentGuid, position,
1769
title, dateAdded, lastModified) AS (
1770
SELECT b1.fk, 0, b1.type, b1.id, b1.guid, b1.parent,
1771
(SELECT guid FROM moz_bookmarks WHERE id = b1.parent),
1772
b1.position, b1.title, b1.dateAdded, b1.lastModified
1773
FROM moz_bookmarks b1 WHERE b1.guid=:item_guid
1774
UNION ALL
1775
SELECT b2.fk, level + 1, b2.type, b2.id, b2.guid, b2.parent,
1776
descendants.guid, b2.position, b2.title, b2.dateAdded,
1777
b2.lastModified
1778
FROM moz_bookmarks b2
1779
JOIN descendants ON b2.parent = descendants.id AND b2.id <> :tags_folder)
1780
SELECT d.level, d.id, d.guid, d.parent, d.parentGuid, d.type,
1781
d.position AS [index], IFNULL(d.title, '') AS title, d.dateAdded,
1782
d.lastModified, h.url, (SELECT icon_url FROM moz_icons i
1783
JOIN moz_icons_to_pages ON icon_id = i.id
1784
JOIN moz_pages_w_icons pi ON page_id = pi.id
1785
WHERE pi.page_url_hash = hash(h.url) AND pi.page_url = h.url
1786
ORDER BY width DESC LIMIT 1) AS iconuri,
1787
(SELECT GROUP_CONCAT(t.title, ',')
1788
FROM moz_bookmarks b2
1789
JOIN moz_bookmarks t ON t.id = +b2.parent AND t.parent = :tags_folder
1790
WHERE b2.fk = h.id
1791
) AS tags,
1792
(SELECT a.content FROM moz_annos a
1793
JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id
1794
WHERE place_id = h.id AND n.name = :charset_anno
1795
) AS charset
1796
FROM descendants d
1797
LEFT JOIN moz_bookmarks b3 ON b3.id = d.parent
1798
LEFT JOIN moz_places h ON h.id = d.fk
1799
ORDER BY d.level, d.parent, d.position`;
1800
1801
if (!aItemGuid) {
1802
aItemGuid = this.bookmarks.rootGuid;
1803
}
1804
1805
let hasExcludeItemsCallback = aOptions.hasOwnProperty(
1806
"excludeItemsCallback"
1807
);
1808
let excludedParents = new Set();
1809
let shouldExcludeItem = (aItem, aParentGuid) => {
1810
let exclude =
1811
excludedParents.has(aParentGuid) ||
1812
aOptions.excludeItemsCallback(aItem);
1813
if (exclude) {
1814
if (aItem.type == this.TYPE_X_MOZ_PLACE_CONTAINER) {
1815
excludedParents.add(aItem.guid);
1816
}
1817
}
1818
return exclude;
1819
};
1820
1821
let rootItem = null;
1822
let parentsMap = new Map();
1823
let conn = await this.promiseDBConnection();
1824
let rows = await conn.executeCached(QUERY_STR, {
1825
tags_folder: PlacesUtils.tagsFolderId,
1826
charset_anno: PlacesUtils.CHARSET_ANNO,
1827
item_guid: aItemGuid,
1828
});
1829
let yieldCounter = 0;
1830
for (let row of rows) {
1831
let item;
1832
if (!rootItem) {
1833
try {
1834
// This is the first row.
1835
rootItem = item = await createItemInfoObject(row, true);
1836
Object.defineProperty(rootItem, "itemsCount", {
1837
value: 1,
1838
writable: true,
1839
enumerable: false,
1840
configurable: false,
1841
});
1842
} catch (ex) {
1843
Cu.reportError("Failed to fetch the data for the root item");
1844
throw ex;
1845
}
1846
} else {
1847
try {
1848
// Our query guarantees that we always visit parents ahead of their
1849
// children.
1850
item = await createItemInfoObject(row, false);
1851
let parentGuid = row.getResultByName("parentGuid");
1852
if (hasExcludeItemsCallback && shouldExcludeItem(item, parentGuid)) {
1853
continue;
1854
}
1855
1856
let parentItem = parentsMap.get(parentGuid);
1857
if ("children" in parentItem) {
1858
parentItem.children.push(item);
1859
} else {
1860
parentItem.children = [item];
1861
}
1862
1863
rootItem.itemsCount++;
1864
} catch (ex) {
1865
// This is a bogus child, report and skip it.
1866
Cu.reportError("Failed to fetch the data for an item " + ex);
1867
continue;
1868
}
1869
}
1870
1871
if (item.type == this.TYPE_X_MOZ_PLACE_CONTAINER) {
1872
parentsMap.set(item.guid, item);
1873
}
1874
1875
// With many bookmarks we end up stealing the CPU - even with yielding!
1876
// So we let everyone else have a go every few items (bug 1186714).
1877
if (++yieldCounter % 50 == 0) {
1878
await new Promise(resolve => {
1879
Services.tm.dispatchToMainThread(resolve);
1880
});
1881
}
1882
}
1883
1884
return rootItem;
1885
},
1886
1887
/**
1888
* Returns a generator that iterates over `array` and yields slices of no
1889
* more than `chunkLength` elements at a time.
1890
*
1891
* @param {Array} array An array containing zero or more elements.
1892
* @param {number} chunkLength The maximum number of elements in each chunk.
1893
* @yields {Array} A chunk of the array.
1894
* @throws if `chunkLength` is negative or not an integer.
1895
*/
1896
*chunkArray(array, chunkLength) {
1897
if (chunkLength <= 0 || !Number.isInteger(chunkLength)) {
1898
throw new TypeError("Chunk length must be a positive integer");
1899
}
1900
if (!array.length) {
1901
return;
1902
}
1903
if (array.length <= chunkLength) {
1904
yield array;
1905
return;
1906
}
1907
let startIndex = 0;
1908
while (startIndex < array.length) {
1909
yield array.slice(startIndex, (startIndex += chunkLength));
1910
}
1911
},
1912
};
1913
1914
XPCOMUtils.defineLazyGetter(PlacesUtils, "history", function() {
1915
let hs = Cc["@mozilla.org/browser/nav-history-service;1"].getService(
1916
Ci.nsINavHistoryService
1917
);
1918
return Object.freeze(
1919
new Proxy(hs, {
1920
get(target, name) {
1921
let property, object;
1922
if (name in target) {
1923
property = target[name];
1924
object = target;
1925
} else {
1926
property = History[name];
1927
object = History;
1928
}
1929
if (typeof property == "function") {
1930
return property.bind(object);
1931
}
1932
return property;
1933
},
1934
})
1935
);
1936
});
1937
1938
XPCOMUtils.defineLazyServiceGetter(
1939
PlacesUtils,
1940
"favicons",
1941
"@mozilla.org/browser/favicon-service;1",
1942
"nsIFaviconService"
1943
);
1944
1945
XPCOMUtils.defineLazyServiceGetter(
1946
this,
1947
"bmsvc",
1948
"@mozilla.org/browser/nav-bookmarks-service;1",
1949
"nsINavBookmarksService"
1950
);
1951
XPCOMUtils.defineLazyGetter(PlacesUtils, "bookmarks", () => {
1952
return Object.freeze(
1953
new Proxy(Bookmarks, {
1954
get: (target, name) =>
1955
Bookmarks.hasOwnProperty(name) ? Bookmarks[name] : bmsvc[name],
1956
})
1957
);
1958
});
1959
1960
XPCOMUtils.defineLazyServiceGetter(
1961
PlacesUtils,
1962
"annotations",
1963
"@mozilla.org/browser/annotation-service;1",
1964
"nsIAnnotationService"
1965
);
1966
1967
XPCOMUtils.defineLazyServiceGetter(
1968
PlacesUtils,
1969
"tagging",
1970
"@mozilla.org/browser/tagging-service;1",
1971
"nsITaggingService"
1972
);
1973
1974
XPCOMUtils.defineLazyGetter(this, "bundle", function() {
1975
const PLACES_STRING_BUNDLE_URI = "chrome://places/locale/places.properties";
1976
return Services.strings.createBundle(PLACES_STRING_BUNDLE_URI);
1977
});
1978
1979
// This is just used as a reasonably-random value for copy & paste / drag operations.
1980
XPCOMUtils.defineLazyGetter(PlacesUtils, "instanceId", () => {
1981
return PlacesUtils.history.makeGuid();
1982
});
1983
1984
/**
1985
* Setup internal databases for closing properly during shutdown.
1986
*
1987
* 1. Places initiates shutdown.
1988
* 2. Before places can move to the step where it closes the low-level connection,
1989
* we need to make sure that we have closed `conn`.
1990
* 3. Before we can close `conn`, we need to make sure that all external clients
1991
* have stopped using `conn`.
1992
* 4. Before we can close Sqlite, we need to close `conn`.
1993
*/
1994
function setupDbForShutdown(conn, name) {
1995
try {
1996
let state = "0. Not started.";
1997
let promiseClosed = new Promise((resolve, reject) => {
1998
// The service initiates shutdown.
1999
// Before it can safely close its connection, we need to make sure
2000
// that we have closed the high-level connection.
2001
try {
2002
PlacesUtils.history.connectionShutdownClient.jsclient.addBlocker(
2003
`${name} closing as part of Places shutdown`,
2004
async function() {
2005
state = "1. Service has initiated shutdown";
2006
2007
// At this stage, all external clients have finished using the
2008
// database. We just need to close the high-level connection.
2009
await conn.close();
2010
state = "2. Closed Sqlite.jsm connection.";
2011
2012
resolve();
2013
},