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
/**
6
* This file works on the old-style "bookmarks.html" file. It includes
7
* functions to import and export existing bookmarks to this file format.
8
*
9
* Format
10
* ------
11
*
12
* Primary heading := h1
13
* Old version used this to set attributes on the bookmarks RDF root, such
14
* as the last modified date. We only use H1 to check for the attribute
15
* PLACES_ROOT, which tells us that this hierarchy root is the places root.
16
* For backwards compatibility, if we don't find this, we assume that the
17
* hierarchy is rooted at the bookmarks menu.
18
* Heading := any heading other than h1
19
* Old version used this to set attributes on the current container. We only
20
* care about the content of the heading container, which contains the title
21
* of the bookmark container.
22
* Bookmark := a
23
* HREF is the destination of the bookmark
24
* FEEDURL is the URI of the RSS feed. This is deprecated and no more
25
* supported, but some old files may still contain it.
26
* LAST_CHARSET is stored as an annotation so that the next time we go to
27
* that page we remember the user's preference.
28
* ICON will be stored in the favicon service
29
* ICON_URI is new for places bookmarks.html, it refers to the original
30
* URI of the favicon so we don't have to make up favicon URLs.
31
* Text of the <a> container is the name of the bookmark
32
* Ignored: LAST_VISIT, ID (writing out non-RDF IDs can confuse Firefox 2)
33
* Bookmark comment := dd
34
* This affects the previosly added bookmark
35
* Separator := hr
36
* Insert a separator into the current container
37
* The folder hierarchy is defined by <dl>/<ul>/<menu> (the old importing code
38
* handles all these cases, when we write, use <dl>).
39
*
40
* Overall design
41
* --------------
42
*
43
* We need to emulate a recursive parser. A "Bookmark import frame" is created
44
* corresponding to each folder we encounter. These are arranged in a stack,
45
* and contain all the state we need to keep track of.
46
*
47
* A frame is created when we find a heading, which defines a new container.
48
* The frame also keeps track of the nesting of <DL>s, (in well-formed
49
* bookmarks files, these will have a 1-1 correspondence with frames, but we
50
* try to be a little more flexible here). When the nesting count decreases
51
* to 0, then we know a frame is complete and to pop back to the previous
52
* frame.
53
*
54
* Note that a lot of things happen when tags are CLOSED because we need to
55
* get the text from the content of the tag. For example, link and heading tags
56
* both require the content (= title) before actually creating it.
57
*/
58
59
var EXPORTED_SYMBOLS = ["BookmarkHTMLUtils"];
60
61
const { XPCOMUtils } = ChromeUtils.import(
63
);
64
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
65
const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
66
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
67
const { FileUtils } = ChromeUtils.import(
69
);
70
const { PlacesUtils } = ChromeUtils.import(
72
);
73
74
XPCOMUtils.defineLazyGlobalGetters(this, ["XMLHttpRequest"]);
75
76
ChromeUtils.defineModuleGetter(
77
this,
78
"PlacesBackups",
80
);
81
82
const Container_Normal = 0;
83
const Container_Toolbar = 1;
84
const Container_Menu = 2;
85
const Container_Unfiled = 3;
86
const Container_Places = 4;
87
88
const MICROSEC_PER_SEC = 1000000;
89
90
const EXPORT_INDENT = " "; // four spaces
91
92
function base64EncodeString(aString) {
93
let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
94
Ci.nsIStringInputStream
95
);
96
stream.setData(aString, aString.length);
97
let encoder = Cc["@mozilla.org/scriptablebase64encoder;1"].createInstance(
98
Ci.nsIScriptableBase64Encoder
99
);
100
return encoder.encodeToString(stream, aString.length);
101
}
102
103
/**
104
* Provides HTML escaping for use in HTML attributes and body of the bookmarks
105
* file, compatible with the old bookmarks system.
106
*/
107
function escapeHtmlEntities(aText) {
108
return (aText || "")
109
.replace(/&/g, "&amp;")
110
.replace(/</g, "&lt;")
111
.replace(/>/g, "&gt;")
112
.replace(/"/g, "&quot;")
113
.replace(/'/g, "&#39;");
114
}
115
116
/**
117
* Provides URL escaping for use in HTML attributes of the bookmarks file,
118
* compatible with the old bookmarks system.
119
*/
120
function escapeUrl(aText) {
121
return (aText || "").replace(/"/g, "%22");
122
}
123
124
function notifyObservers(aTopic, aInitialImport) {
125
Services.obs.notifyObservers(
126
null,
127
aTopic,
128
aInitialImport ? "html-initial" : "html"
129
);
130
}
131
132
var BookmarkHTMLUtils = Object.freeze({
133
/**
134
* Loads the current bookmarks hierarchy from a "bookmarks.html" file.
135
*
136
* @param aSpec
137
* String containing the "file:" URI for the existing "bookmarks.html"
138
* file to be loaded.
139
* @param [options.replace]
140
* Whether we should erase existing bookmarks before loading.
141
* Defaults to `false`.
142
* @param [options.source]
143
* The bookmark change source, used to determine the sync status for
144
* imported bookmarks. Defaults to `RESTORE` if `replace = true`, or
145
* `IMPORT` otherwise.
146
*
147
* @return {Promise}
148
* @resolves When the new bookmarks have been created.
149
* @rejects JavaScript exception.
150
*/
151
async importFromURL(
152
aSpec,
153
{
154
replace: aInitialImport = false,
155
source: aSource = aInitialImport
156
? PlacesUtils.bookmarks.SOURCES.RESTORE
157
: PlacesUtils.bookmarks.SOURCES.IMPORT,
158
} = {}
159
) {
160
notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN, aInitialImport);
161
try {
162
let importer = new BookmarkImporter(aInitialImport, aSource);
163
await importer.importFromURL(aSpec);
164
165
notifyObservers(
166
PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS,
167
aInitialImport
168
);
169
} catch (ex) {
170
Cu.reportError("Failed to import bookmarks from " + aSpec + ": " + ex);
171
notifyObservers(
172
PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED,
173
aInitialImport
174
);
175
throw ex;
176
}
177
},
178
179
/**
180
* Loads the current bookmarks hierarchy from a "bookmarks.html" file.
181
*
182
* @param aFilePath
183
* OS.File path string of the "bookmarks.html" file to be loaded.
184
* @param [options.replace]
185
* Whether we should erase existing bookmarks before loading.
186
* Defaults to `false`.
187
* @param [options.source]
188
* The bookmark change source, used to determine the sync status for
189
* imported bookmarks. Defaults to `RESTORE` if `replace = true`, or
190
* `IMPORT` otherwise.
191
*
192
* @return {Promise}
193
* @resolves When the new bookmarks have been created.
194
* @rejects JavaScript exception.
195
*/
196
async importFromFile(
197
aFilePath,
198
{
199
replace: aInitialImport = false,
200
source: aSource = aInitialImport
201
? PlacesUtils.bookmarks.SOURCES.RESTORE
202
: PlacesUtils.bookmarks.SOURCES.IMPORT,
203
} = {}
204
) {
205
notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN, aInitialImport);
206
try {
207
if (!(await OS.File.exists(aFilePath))) {
208
throw new Error(
209
"Cannot import from nonexisting html file: " + aFilePath
210
);
211
}
212
let importer = new BookmarkImporter(aInitialImport, aSource);
213
await importer.importFromURL(OS.Path.toFileURI(aFilePath));
214
215
notifyObservers(
216
PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS,
217
aInitialImport
218
);
219
} catch (ex) {
220
Cu.reportError(
221
"Failed to import bookmarks from " + aFilePath + ": " + ex
222
);
223
notifyObservers(
224
PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED,
225
aInitialImport
226
);
227
throw ex;
228
}
229
},
230
231
/**
232
* Saves the current bookmarks hierarchy to a "bookmarks.html" file.
233
*
234
* @param aFilePath
235
* OS.File path string for the "bookmarks.html" file to be created.
236
*
237
* @return {Promise}
238
* @resolves To the exported bookmarks count when the file has been created.
239
* @rejects JavaScript exception.
240
*/
241
async exportToFile(aFilePath) {
242
let [bookmarks, count] = await PlacesBackups.getBookmarksTree();
243
let startTime = Date.now();
244
245
// Report the time taken to convert the tree to HTML.
246
let exporter = new BookmarkExporter(bookmarks);
247
await exporter.exportToFile(aFilePath);
248
249
try {
250
Services.telemetry
251
.getHistogramById("PLACES_EXPORT_TOHTML_MS")
252
.add(Date.now() - startTime);
253
} catch (ex) {
254
Cu.reportError("Unable to report telemetry.");
255
}
256
257
return count;
258
},
259
260
get defaultPath() {
261
try {
262
return Services.prefs.getCharPref("browser.bookmarks.file");
263
} catch (ex) {}
264
return OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.html");
265
},
266
});
267
268
function Frame(aFolder) {
269
this.folder = aFolder;
270
271
/**
272
* How many <dl>s have been nested. Each frame/container should start
273
* with a heading, and is then followed by a <dl>, <ul>, or <menu>. When
274
* that list is complete, then it is the end of this container and we need
275
* to pop back up one level for new items. If we never get an open tag for
276
* one of these things, we should assume that the container is empty and
277
* that things we find should be siblings of it. Normally, these <dl>s won't
278
* be nested so this will be 0 or 1.
279
*/
280
this.containerNesting = 0;
281
282
/**
283
* when we find a heading tag, it actually affects the title of the NEXT
284
* container in the list. This stores that heading tag and whether it was
285
* special. 'consumeHeading' resets this._
286
*/
287
this.lastContainerType = Container_Normal;
288
289
/**
290
* this contains the text from the last begin tag until now. It is reset
291
* at every begin tag. We can check it when we see a </a>, or </h3>
292
* to see what the text content of that node should be.
293
*/
294
this.previousText = "";
295
296
/**
297
* true when we hit a <dd>, which contains the description for the preceding
298
* <a> tag. We can't just check for </dd> like we can for </a> or </h3>
299
* because if there is a sub-folder, it is actually a child of the <dd>
300
* because the tag is never explicitly closed. If this is true and we see a
301
* new open tag, that means to commit the description to the previous
302
* bookmark.
303
*
304
* Additional weirdness happens when the previous <dt> tag contains a <h3>:
305
* this means there is a new folder with the given description, and whose
306
* children are contained in the following <dl> list.
307
*
308
* This is handled in openContainer(), which commits previous text if
309
* necessary.
310
*/
311
this.inDescription = false;
312
313
/**
314
* contains the URL of the previous bookmark created. This is used so that
315
* when we encounter a <dd>, we know what bookmark to associate the text with.
316
* This is cleared whenever we hit a <h3>, so that we know NOT to save this
317
* with a bookmark, but to keep it until
318
*/
319
this.previousLink = null;
320
321
/**
322
* Contains a reference to the last created bookmark or folder object.
323
*/
324
this.previousItem = null;
325
326
/**
327
* Contains the date-added and last-modified-date of an imported item.
328
* Used to override the values set by insertBookmark, createFolder, etc.
329
*/
330
this.previousDateAdded = null;
331
this.previousLastModifiedDate = null;
332
}
333
334
function BookmarkImporter(aInitialImport, aSource) {
335
this._isImportDefaults = aInitialImport;
336
this._source = aSource;
337
338
// This root is where we construct the bookmarks tree into, following the format
339
// of the imported file.
340
// If we're doing an initial import, the non-menu roots will be created as
341
// children of this root, so in _getBookmarkTrees we'll split them out.
342
// If we're not doing an initial import, everything gets imported under the
343
// bookmark menu folder, so there won't be any need for _getBookmarkTrees to
344
// do separation.
345
this._bookmarkTree = {
346
type: PlacesUtils.bookmarks.TYPE_FOLDER,
347
guid: PlacesUtils.bookmarks.menuGuid,
348
children: [],
349
};
350
351
this._frames = [];
352
this._frames.push(new Frame(this._bookmarkTree));
353
}
354
355
BookmarkImporter.prototype = {
356
_safeTrim: function safeTrim(aStr) {
357
return aStr ? aStr.trim() : aStr;
358
},
359
360
get _curFrame() {
361
return this._frames[this._frames.length - 1];
362
},
363
364
get _previousFrame() {
365
return this._frames[this._frames.length - 2];
366
},
367
368
/**
369
* This is called when there is a new folder found. The folder takes the
370
* name from the previous frame's heading.
371
*/
372
_newFrame: function newFrame() {
373
let frame = this._curFrame;
374
let containerTitle = frame.previousText;
375
frame.previousText = "";
376
let containerType = frame.lastContainerType;
377
378
let folder = {
379
children: [],
380
type: PlacesUtils.bookmarks.TYPE_FOLDER,
381
};
382
383
switch (containerType) {
384
case Container_Normal:
385
// This can only be a sub-folder so no need to set a guid here.
386
folder.title = containerTitle;
387
break;
388
case Container_Places:
389
folder.guid = PlacesUtils.bookmarks.rootGuid;
390
break;
391
case Container_Menu:
392
folder.guid = PlacesUtils.bookmarks.menuGuid;
393
break;
394
case Container_Unfiled:
395
folder.guid = PlacesUtils.bookmarks.unfiledGuid;
396
break;
397
case Container_Toolbar:
398
folder.guid = PlacesUtils.bookmarks.toolbarGuid;
399
break;
400
default:
401
// NOT REACHED
402
throw new Error("Unknown bookmark container type!");
403
}
404
405
frame.folder.children.push(folder);
406
407
if (frame.previousDateAdded != null) {
408
folder.dateAdded = frame.previousDateAdded;
409
frame.previousDateAdded = null;
410
}
411
412
if (frame.previousLastModifiedDate != null) {
413
folder.lastModified = frame.previousLastModifiedDate;
414
frame.previousLastModifiedDate = null;
415
}
416
417
if (
418
!folder.hasOwnProperty("dateAdded") &&
419
folder.hasOwnProperty("lastModified")
420
) {
421
folder.dateAdded = folder.lastModified;
422
}
423
424
frame.previousItem = folder;
425
426
this._frames.push(new Frame(folder));
427
},
428
429
/**
430
* Handles <hr> as a separator.
431
*
432
* @note Separators may have a title in old html files, though Places dropped
433
* support for them.
434
* We also don't import ADD_DATE or LAST_MODIFIED for separators because
435
* pre-Places bookmarks did not support them.
436
*/
437
_handleSeparator: function handleSeparator(aElt) {
438
let frame = this._curFrame;
439
440
let separator = {
441
type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
442
};
443
frame.folder.children.push(separator);
444
frame.previousItem = separator;
445
},
446
447
/**
448
* Called for h2,h3,h4,h5,h6. This just stores the correct information in
449
* the current frame; the actual new frame corresponding to the container
450
* associated with the heading will be created when the tag has been closed
451
* and we know the title (we don't know to create a new folder or to merge
452
* with an existing one until we have the title).
453
*/
454
_handleHeadBegin: function handleHeadBegin(aElt) {
455
let frame = this._curFrame;
456
457
// after a heading, a previous bookmark is not applicable (for example, for
458
// the descriptions contained in a <dd>). Neither is any previous head type
459
frame.previousLink = null;
460
frame.lastContainerType = Container_Normal;
461
462
// It is syntactically possible for a heading to appear after another heading
463
// but before the <dl> that encloses that folder's contents. This should not
464
// happen in practice, as the file will contain "<dl></dl>" sequence for
465
// empty containers.
466
//
467
// Just to be on the safe side, if we encounter
468
// <h3>FOO</h3>
469
// <h3>BAR</h3>
470
// <dl>...content 1...</dl>
471
// <dl>...content 2...</dl>
472
// we'll pop the stack when we find the h3 for BAR, treating that as an
473
// implicit ending of the FOO container. The output will be FOO and BAR as
474
// siblings. If there's another <dl> following (as in "content 2"), those
475
// items will be treated as further siblings of FOO and BAR
476
// This special frame popping business, of course, only happens when our
477
// frame array has more than one element so we can avoid situations where
478
// we don't have a frame to parse into anymore.
479
if (frame.containerNesting == 0 && this._frames.length > 1) {
480
this._frames.pop();
481
}
482
483
// We have to check for some attributes to see if this is a "special"
484
// folder, which will have different creation rules when the end tag is
485
// processed.
486
if (aElt.hasAttribute("personal_toolbar_folder")) {
487
if (this._isImportDefaults) {
488
frame.lastContainerType = Container_Toolbar;
489
}
490
} else if (aElt.hasAttribute("bookmarks_menu")) {
491
if (this._isImportDefaults) {
492
frame.lastContainerType = Container_Menu;
493
}
494
} else if (aElt.hasAttribute("unfiled_bookmarks_folder")) {
495
if (this._isImportDefaults) {
496
frame.lastContainerType = Container_Unfiled;
497
}
498
} else if (aElt.hasAttribute("places_root")) {
499
if (this._isImportDefaults) {
500
frame.lastContainerType = Container_Places;
501
}
502
} else {
503
let addDate = aElt.getAttribute("add_date");
504
if (addDate) {
505
frame.previousDateAdded = this._convertImportedDateToInternalDate(
506
addDate
507
);
508
}
509
let modDate = aElt.getAttribute("last_modified");
510
if (modDate) {
511
frame.previousLastModifiedDate = this._convertImportedDateToInternalDate(
512
modDate
513
);
514
}
515
}
516
this._curFrame.previousText = "";
517
},
518
519
/*
520
* Handles "<a" tags by creating a new bookmark. The title of the bookmark
521
* will be the text content, which will be stuffed in previousText for us
522
* and which will be saved by handleLinkEnd
523
*/
524
_handleLinkBegin: function handleLinkBegin(aElt) {
525
let frame = this._curFrame;
526
527
frame.previousItem = null;
528
frame.previousText = ""; // Will hold link text, clear it.
529
530
// Get the attributes we care about.
531
let href = this._safeTrim(aElt.getAttribute("href"));
532
let icon = this._safeTrim(aElt.getAttribute("icon"));
533
let iconUri = this._safeTrim(aElt.getAttribute("icon_uri"));
534
let lastCharset = this._safeTrim(aElt.getAttribute("last_charset"));
535
let keyword = this._safeTrim(aElt.getAttribute("shortcuturl"));
536
let postData = this._safeTrim(aElt.getAttribute("post_data"));
537
let dateAdded = this._safeTrim(aElt.getAttribute("add_date"));
538
let lastModified = this._safeTrim(aElt.getAttribute("last_modified"));
539
let tags = this._safeTrim(aElt.getAttribute("tags"));
540
541
// Ignore <a> tags that have no href.
542
try {
543
frame.previousLink = Services.io.newURI(href).spec;
544
} catch (e) {
545
frame.previousLink = null;
546
return;
547
}
548
549
let bookmark = {};
550
551
// Only set the url for bookmarks.
552
if (frame.previousLink) {
553
bookmark.url = frame.previousLink;
554
}
555
556
if (dateAdded) {
557
bookmark.dateAdded = this._convertImportedDateToInternalDate(dateAdded);
558
}
559
// Save bookmark's last modified date.
560
if (lastModified) {
561
bookmark.lastModified = this._convertImportedDateToInternalDate(
562
lastModified
563
);
564
}
565
566
if (!dateAdded && lastModified) {
567
bookmark.dateAdded = bookmark.lastModified;
568
}
569
570
if (tags) {
571
bookmark.tags = tags
572
.split(",")
573
.filter(
574
aTag =>
575
!!aTag.length && aTag.length <= PlacesUtils.bookmarks.MAX_TAG_LENGTH
576
);
577
578
// If we end up with none, then delete the property completely.
579
if (!bookmark.tags.length) {
580
delete bookmark.tags;
581
}
582
}
583
584
if (lastCharset) {
585
bookmark.charset = lastCharset;
586
}
587
588
if (keyword) {
589
bookmark.keyword = keyword;
590
}
591
592
if (postData) {
593
bookmark.postData = postData;
594
}
595
596
if (icon) {
597
bookmark.icon = icon;
598
}
599
600
if (iconUri) {
601
bookmark.iconUri = iconUri;
602
}
603
604
// Add bookmark to the tree.
605
frame.folder.children.push(bookmark);
606
frame.previousItem = bookmark;
607
},
608
609
_handleContainerBegin: function handleContainerBegin() {
610
this._curFrame.containerNesting++;
611
},
612
613
/**
614
* Our "indent" count has decreased, and when we hit 0 that means that this
615
* container is complete and we need to pop back to the outer frame. Never
616
* pop the toplevel frame
617
*/
618
_handleContainerEnd: function handleContainerEnd() {
619
let frame = this._curFrame;
620
if (frame.containerNesting > 0) {
621
frame.containerNesting--;
622
}
623
if (this._frames.length > 1 && frame.containerNesting == 0) {
624
this._frames.pop();
625
}
626
},
627
628
/**
629
* Creates the new frame for this heading now that we know the name of the
630
* container (tokens since the heading open tag will have been placed in
631
* previousText).
632
*/
633
_handleHeadEnd: function handleHeadEnd() {
634
this._newFrame();
635
},
636
637
/**
638
* Saves the title for the given bookmark.
639
*/
640
_handleLinkEnd: function handleLinkEnd() {
641
let frame = this._curFrame;
642
frame.previousText = frame.previousText.trim();
643
644
if (frame.previousItem != null) {
645
frame.previousItem.title = frame.previousText;
646
}
647
648
frame.previousText = "";
649
},
650
651
_openContainer: function openContainer(aElt) {
652
if (aElt.namespaceURI != "http://www.w3.org/1999/xhtml") {
653
return;
654
}
655
switch (aElt.localName) {
656
case "h2":
657
case "h3":
658
case "h4":
659
case "h5":
660
case "h6":
661
this._handleHeadBegin(aElt);
662
break;
663
case "a":
664
this._handleLinkBegin(aElt);
665
break;
666
case "dl":
667
case "ul":
668
case "menu":
669
this._handleContainerBegin();
670
break;
671
case "dd":
672
this._curFrame.inDescription = true;
673
break;
674
case "hr":
675
this._handleSeparator(aElt);
676
break;
677
}
678
},
679
680
_closeContainer: function closeContainer(aElt) {
681
let frame = this._curFrame;
682
683
// Although we no longer support importing descriptions, we still need to
684
// clear any previous text, so that it doesn't get swallowed into other elements.
685
if (frame.inDescription) {
686
frame.previousText = "";
687
frame.inDescription = false;
688
}
689
690
if (aElt.namespaceURI != "http://www.w3.org/1999/xhtml") {
691
return;
692
}
693
switch (aElt.localName) {
694
case "dl":
695
case "ul":
696
case "menu":
697
this._handleContainerEnd();
698
break;
699
case "dt":
700
break;
701
case "h1":
702
// ignore
703
break;
704
case "h2":
705
case "h3":
706
case "h4":
707
case "h5":
708
case "h6":
709
this._handleHeadEnd();
710
break;
711
case "a":
712
this._handleLinkEnd();
713
break;
714
default:
715
break;
716
}
717
},
718
719
_appendText: function appendText(str) {
720
this._curFrame.previousText += str;
721
},
722
723
/**
724
* Converts a string date in seconds to a date object
725
*/
726
_convertImportedDateToInternalDate: function convertImportedDateToInternalDate(
727
aDate
728
) {
729
try {
730
if (aDate && !isNaN(aDate)) {
731
return new Date(parseInt(aDate) * 1000); // in bookmarks.html this value is in seconds
732
}
733
} catch (ex) {
734
// Do nothing.
735
}
736
return new Date();
737
},
738
739
_walkTreeForImport(aDoc) {
740
if (!aDoc) {
741
return;
742
}
743
744
let current = aDoc;
745
let next;
746
for (;;) {
747
switch (current.nodeType) {
748
case current.ELEMENT_NODE:
749
this._openContainer(current);
750
break;
751
case current.TEXT_NODE:
752
this._appendText(current.data);
753
break;
754
}
755
if ((next = current.firstChild)) {
756
current = next;
757
continue;
758
}
759
for (;;) {
760
if (current.nodeType == current.ELEMENT_NODE) {
761
this._closeContainer(current);
762
}
763
if (current == aDoc) {
764
return;
765
}
766
if ((next = current.nextSibling)) {
767
current = next;
768
break;
769
}
770
current = current.parentNode;
771
}
772
}
773
},
774
775
/**
776
* Returns the bookmark tree(s) from the importer. These are suitable for
777
* passing to PlacesUtils.bookmarks.insertTree().
778
*
779
* @returns {Array} An array of bookmark trees.
780
*/
781
_getBookmarkTrees() {
782
// If we're not importing defaults, then everything gets imported under the
783
// Bookmarks menu.
784
if (!this._isImportDefaults) {
785
return [this._bookmarkTree];
786
}
787
788
// If we are importing defaults, we need to separate out the top-level
789
// default folders into separate items, for the caller to pass into insertTree.
790
let bookmarkTrees = [this._bookmarkTree];
791
792
// The children of this "root" element will contain normal children of the
793
// bookmark menu as well as the places roots. Hence, we need to filter out
794
// the separate roots, but keep the children that are relevant to the
795
// bookmark menu.
796
this._bookmarkTree.children = this._bookmarkTree.children.filter(child => {
797
if (
798
child.guid &&
799
PlacesUtils.bookmarks.userContentRoots.includes(child.guid)
800
) {
801
bookmarkTrees.push(child);
802
return false;
803
}
804
return true;
805
});
806
807
return bookmarkTrees;
808
},
809
810
/**
811
* Imports the bookmarks from the importer into the places database.
812
*
813
* @param {BookmarkImporter} importer The importer from which to get the
814
* bookmark information.
815
*/
816
async _importBookmarks() {
817
if (this._isImportDefaults) {
818
await PlacesUtils.bookmarks.eraseEverything();
819
}
820
821
let bookmarksTrees = this._getBookmarkTrees();
822
for (let tree of bookmarksTrees) {
823
if (!tree.children.length) {
824
continue;
825
}
826
827
// Give the tree the source.
828
tree.source = this._source;
829
await PlacesUtils.bookmarks.insertTree(tree, {
830
fixupOrSkipInvalidEntries: true,
831
});
832
insertFaviconsForTree(tree);
833
}
834
},
835
836
/**
837
* Imports data into the places database from the supplied url.
838
*
839
* @param {String} href The url to import data from.
840
*/
841
async importFromURL(href) {
842
let data = await fetchData(href);
843
this._walkTreeForImport(data);
844
await this._importBookmarks();
845
},
846
};
847
848
function BookmarkExporter(aBookmarksTree) {
849
// Create a map of the roots.
850
let rootsMap = new Map();
851
for (let child of aBookmarksTree.children) {
852
if (child.root) {
853
rootsMap.set(child.root, child);
854
// Also take the opportunity to get the correctly localised title for the
855
// root.
856
child.title = PlacesUtils.bookmarks.getLocalizedTitle(child);
857
}
858
}
859
860
// For backwards compatibility reasons the bookmarks menu is the root, while
861
// the bookmarks toolbar and unfiled bookmarks will be child items.
862
this._root = rootsMap.get("bookmarksMenuFolder");
863
864
for (let key of ["toolbarFolder", "unfiledBookmarksFolder"]) {
865
let root = rootsMap.get(key);
866
if (root.children && root.children.length) {
867
if (!this._root.children) {
868
this._root.children = [];
869
}
870
this._root.children.push(root);
871
}
872
}
873
}
874
875
BookmarkExporter.prototype = {
876
exportToFile: function exportToFile(aFilePath) {
877
return (async () => {
878
// Create a file that can be accessed by the current user only.
879
let out = FileUtils.openAtomicFileOutputStream(
880
new FileUtils.File(aFilePath)
881
);
882
try {
883
// We need a buffered output stream for performance. See bug 202477.
884
let bufferedOut = Cc[
885
"@mozilla.org/network/buffered-output-stream;1"
886
].createInstance(Ci.nsIBufferedOutputStream);
887
bufferedOut.init(out, 4096);
888
try {
889
// Write bookmarks in UTF-8.
890
this._converterOut = Cc[
891
"@mozilla.org/intl/converter-output-stream;1"
892
].createInstance(Ci.nsIConverterOutputStream);
893
this._converterOut.init(bufferedOut, "utf-8");
894
try {
895
this._writeHeader();
896
await this._writeContainer(this._root);
897
// Retain the target file on success only.
898
bufferedOut.QueryInterface(Ci.nsISafeOutputStream).finish();
899
} finally {
900
this._converterOut.close();
901
this._converterOut = null;
902
}
903
} finally {
904
bufferedOut.close();
905
}
906
} finally {
907
out.close();
908
}
909
})();
910
},
911
912
_converterOut: null,
913
914
_write(aText) {
915
this._converterOut.writeString(aText || "");
916
},
917
918
_writeAttribute(aName, aValue) {
919
this._write(" " + aName + '="' + aValue + '"');
920
},
921
922
_writeLine(aText) {
923
this._write(aText + "\n");
924
},
925
926
_writeHeader() {
927
this._writeLine("<!DOCTYPE NETSCAPE-Bookmark-file-1>");
928
this._writeLine("<!-- This is an automatically generated file.");
929
this._writeLine(" It will be read and overwritten.");
930
this._writeLine(" DO NOT EDIT! -->");
931
this._writeLine(
932
'<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">'
933
);
934
this._writeLine("<TITLE>Bookmarks</TITLE>");
935
},
936
937
async _writeContainer(aItem, aIndent = "") {
938
if (aItem == this._root) {
939
this._writeLine("<H1>" + escapeHtmlEntities(this._root.title) + "</H1>");
940
this._writeLine("");
941
} else {
942
this._write(aIndent + "<DT><H3");
943
this._writeDateAttributes(aItem);
944
945
if (aItem.root === "toolbarFolder") {
946
this._writeAttribute("PERSONAL_TOOLBAR_FOLDER", "true");
947
} else if (aItem.root === "unfiledBookmarksFolder") {
948
this._writeAttribute("UNFILED_BOOKMARKS_FOLDER", "true");
949
}
950
this._writeLine(">" + escapeHtmlEntities(aItem.title) + "</H3>");
951
}
952
953
this._writeLine(aIndent + "<DL><p>");
954
if (aItem.children) {
955
await this._writeContainerContents(aItem, aIndent);
956
}
957
if (aItem == this._root) {
958
this._writeLine(aIndent + "</DL>");
959
} else {
960
this._writeLine(aIndent + "</DL><p>");
961
}
962
},
963
964
async _writeContainerContents(aItem, aIndent) {
965
let localIndent = aIndent + EXPORT_INDENT;
966
967
for (let child of aItem.children) {
968
if (child.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
969
await this._writeContainer(child, localIndent);
970
} else if (child.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) {
971
this._writeSeparator(child, localIndent);
972
} else {
973
await this._writeItem(child, localIndent);
974
}
975
}
976
},
977
978
_writeSeparator(aItem, aIndent) {
979
this._write(aIndent + "<HR");
980
// We keep exporting separator titles, but don't support them anymore.
981
if (aItem.title) {
982
this._writeAttribute("NAME", escapeHtmlEntities(aItem.title));
983
}
984
this._write(">");
985
},
986
987
async _writeItem(aItem, aIndent) {
988
try {
989
NetUtil.newURI(aItem.uri);
990
} catch (ex) {
991
// If the item URI is invalid, skip the item instead of failing later.
992
return;
993
}
994
995
this._write(aIndent + "<DT><A");
996
this._writeAttribute("HREF", escapeUrl(aItem.uri));
997
this._writeDateAttributes(aItem);
998
await this._writeFaviconAttribute(aItem);
999
1000
if (aItem.keyword) {
1001
this._writeAttribute("SHORTCUTURL", escapeHtmlEntities(aItem.keyword));
1002
if (aItem.postData) {
1003
this._writeAttribute("POST_DATA", escapeHtmlEntities(aItem.postData));
1004
}
1005
}
1006
1007
if (aItem.charset) {
1008
this._writeAttribute("LAST_CHARSET", escapeHtmlEntities(aItem.charset));
1009
}
1010
if (aItem.tags) {
1011
this._writeAttribute("TAGS", escapeHtmlEntities(aItem.tags));
1012
}
1013
this._writeLine(">" + escapeHtmlEntities(aItem.title) + "</A>");
1014
},
1015
1016
_writeDateAttributes(aItem) {
1017
if (aItem.dateAdded) {
1018
this._writeAttribute(
1019
"ADD_DATE",
1020
Math.floor(aItem.dateAdded / MICROSEC_PER_SEC)
1021
);
1022
}
1023
if (aItem.lastModified) {
1024
this._writeAttribute(
1025
"LAST_MODIFIED",
1026
Math.floor(aItem.lastModified / MICROSEC_PER_SEC)
1027
);
1028
}
1029
},
1030
1031
async _writeFaviconAttribute(aItem) {
1032
if (!aItem.iconuri) {
1033
return;
1034
}
1035
let favicon;
1036
try {
1037
favicon = await PlacesUtils.promiseFaviconData(aItem.uri);
1038
} catch (ex) {
1039
Cu.reportError("Unexpected Error trying to fetch icon data");
1040
return;
1041
}
1042
1043
this._writeAttribute("ICON_URI", escapeUrl(favicon.uri.spec));
1044
1045
if (!favicon.uri.schemeIs("chrome") && favicon.dataLen > 0) {
1046
let faviconContents =
1047
"data:image/png;base64," +
1048
base64EncodeString(String.fromCharCode.apply(String, favicon.data));
1049
this._writeAttribute("ICON", faviconContents);
1050
}
1051
},
1052
};
1053
1054
/**
1055
* Handles inserting favicons into the database for a bookmark node.
1056
* It is assumed the node has already been inserted into the bookmarks
1057
* database.
1058
*
1059
* @param {Object} node The bookmark node for icons to be inserted.
1060
*/
1061
function insertFaviconForNode(node) {
1062
if (node.icon) {
1063
try {
1064
// Create a fake faviconURI to use (FIXME: bug 523932)
1065
let faviconURI = Services.io.newURI("fake-favicon-uri:" + node.url);
1066
PlacesUtils.favicons.replaceFaviconDataFromDataURL(
1067
faviconURI,
1068
node.icon,
1069
0,
1070
Services.scriptSecurityManager.getSystemPrincipal()
1071
);
1072
PlacesUtils.favicons.setAndFetchFaviconForPage(
1073
Services.io.newURI(node.url),
1074
faviconURI,
1075
false,
1076
PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
1077
null,
1078
Services.scriptSecurityManager.getSystemPrincipal()
1079
);
1080
} catch (ex) {
1081
Cu.reportError("Failed to import favicon data:" + ex);
1082
}
1083
}
1084
1085
if (!node.iconUri) {
1086
return;
1087
}
1088
1089
try {
1090
PlacesUtils.favicons.setAndFetchFaviconForPage(
1091
Services.io.newURI(node.url),
1092
Services.io.newURI(node.iconUri),
1093
false,
1094
PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
1095
null,
1096
Services.scriptSecurityManager.getSystemPrincipal()
1097
);
1098
} catch (ex) {
1099
Cu.reportError("Failed to import favicon URI:" + ex);
1100
}
1101
}
1102
1103
/**
1104
* Handles inserting favicons into the database for a bookmark tree - a node
1105
* and its children.
1106
*
1107
* It is assumed the nodes have already been inserted into the bookmarks
1108
* database.
1109
*
1110
* @param {Object} nodeTree The bookmark node tree for icons to be inserted.
1111
*/
1112
function insertFaviconsForTree(nodeTree) {
1113
insertFaviconForNode(nodeTree);
1114
1115
if (nodeTree.children) {
1116
for (let child of nodeTree.children) {
1117
insertFaviconsForTree(child);
1118
}
1119
}
1120
}
1121
1122
/**
1123
* Handles fetching data from a URL.
1124
*
1125
* @param {String} href The url to fetch data from.
1126
* @return {Promise} Returns a promise that is resolved with the data once
1127
* the fetch is complete, or is rejected if it fails.
1128
*/
1129
function fetchData(href) {
1130
return new Promise((resolve, reject) => {
1131
let xhr = new XMLHttpRequest();
1132
xhr.onload = () => {
1133
resolve(xhr.responseXML);
1134
};
1135
xhr.onabort = xhr.onerror = xhr.ontimeout = () => {
1136
reject(new Error("xmlhttprequest failed"));
1137
};
1138
xhr.open("GET", href);
1139
xhr.responseType = "document";
1140
xhr.overrideMimeType("text/html");
1141
xhr.send();
1142
});
1143
}