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/. */
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.
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.
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
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>).
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.
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
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.
59 import { NetUtil } from "resource://gre/modules/NetUtil.sys.mjs";
61 import { FileUtils } from "resource://gre/modules/FileUtils.sys.mjs";
62 import { PlacesUtils } from "resource://gre/modules/PlacesUtils.sys.mjs";
66 ChromeUtils.defineESModuleGetters(lazy, {
67 PlacesBackups: "resource://gre/modules/PlacesBackups.sys.mjs",
70 const Container_Normal = 0;
71 const Container_Toolbar = 1;
72 const Container_Menu = 2;
73 const Container_Unfiled = 3;
74 const Container_Places = 4;
76 const MICROSEC_PER_SEC = 1000000;
78 const EXPORT_INDENT = " "; // four spaces
80 function base64EncodeString(aString) {
81 let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
82 Ci.nsIStringInputStream
84 stream.setData(aString, aString.length);
85 let encoder = Cc["@mozilla.org/scriptablebase64encoder;1"].createInstance(
86 Ci.nsIScriptableBase64Encoder
88 return encoder.encodeToString(stream, aString.length);
92 * Provides HTML escaping for use in HTML attributes and body of the bookmarks
93 * file, compatible with the old bookmarks system.
95 function escapeHtmlEntities(aText) {
97 .replace(/&/g, "&")
98 .replace(/</g, "<")
99 .replace(/>/g, ">")
100 .replace(/"/g, """)
101 .replace(/'/g, "'");
105 * Provides URL escaping for use in HTML attributes of the bookmarks file,
106 * compatible with the old bookmarks system.
108 function escapeUrl(aText) {
109 return (aText || "").replace(/"/g, "%22");
112 function notifyObservers(aTopic, aInitialImport) {
113 Services.obs.notifyObservers(
116 aInitialImport ? "html-initial" : "html"
120 export var BookmarkHTMLUtils = Object.freeze({
122 * Loads the current bookmarks hierarchy from a "bookmarks.html" file.
125 * String containing the "file:" URI for the existing "bookmarks.html"
127 * @param [options.replace]
128 * Whether we should erase existing bookmarks before loading.
129 * Defaults to `false`.
130 * @param [options.source]
131 * The bookmark change source, used to determine the sync status for
132 * imported bookmarks. Defaults to `RESTORE` if `replace = true`, or
133 * `IMPORT` otherwise.
135 * @returns {Promise<number>} The number of imported bookmarks, not including
136 * folders and separators.
137 * @resolves When the new bookmarks have been created.
138 * @rejects JavaScript exception.
143 replace: aInitialImport = false,
144 source: aSource = aInitialImport
145 ? PlacesUtils.bookmarks.SOURCES.RESTORE
146 : PlacesUtils.bookmarks.SOURCES.IMPORT,
150 notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN, aInitialImport);
152 let importer = new BookmarkImporter(aInitialImport, aSource);
153 bookmarkCount = await importer.importFromURL(aSpec);
156 PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS,
160 console.error(`Failed to import bookmarks from ${aSpec}:`, ex);
162 PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED,
167 return bookmarkCount;
171 * Loads the current bookmarks hierarchy from a "bookmarks.html" file.
174 * OS.File path string of the "bookmarks.html" file to be loaded.
175 * @param [options.replace]
176 * Whether we should erase existing bookmarks before loading.
177 * Defaults to `false`.
178 * @param [options.source]
179 * The bookmark change source, used to determine the sync status for
180 * imported bookmarks. Defaults to `RESTORE` if `replace = true`, or
181 * `IMPORT` otherwise.
183 * @returns {Promise<number>} The number of imported bookmarks, not including
184 * folders and separators
185 * @resolves When the new bookmarks have been created.
186 * @rejects JavaScript exception.
188 async importFromFile(
191 replace: aInitialImport = false,
192 source: aSource = aInitialImport
193 ? PlacesUtils.bookmarks.SOURCES.RESTORE
194 : PlacesUtils.bookmarks.SOURCES.IMPORT,
198 notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN, aInitialImport);
200 if (!(await IOUtils.exists(aFilePath))) {
202 "Cannot import from nonexisting html file: " + aFilePath
205 let importer = new BookmarkImporter(aInitialImport, aSource);
206 bookmarkCount = await importer.importFromURL(
207 PathUtils.toFileURI(aFilePath)
211 PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS,
215 console.error(`Failed to import bookmarks from ${aFilePath}:`, ex);
217 PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED,
222 return bookmarkCount;
226 * Saves the current bookmarks hierarchy to a "bookmarks.html" file.
229 * OS.File path string for the "bookmarks.html" file to be created.
232 * @resolves To the exported bookmarks count when the file has been created.
233 * @rejects JavaScript exception.
235 async exportToFile(aFilePath) {
236 let [bookmarks, count] = await lazy.PlacesBackups.getBookmarksTree();
237 let startTime = Date.now();
239 // Report the time taken to convert the tree to HTML.
240 let exporter = new BookmarkExporter(bookmarks);
241 await exporter.exportToFile(aFilePath);
245 .getHistogramById("PLACES_EXPORT_TOHTML_MS")
246 .add(Date.now() - startTime);
248 console.error("Unable to report telemetry.");
256 return Services.prefs.getCharPref("browser.bookmarks.file");
258 return PathUtils.join(PathUtils.profileDir, "bookmarks.html");
262 function Frame(aFolder) {
263 this.folder = aFolder;
266 * How many <dl>s have been nested. Each frame/container should start
267 * with a heading, and is then followed by a <dl>, <ul>, or <menu>. When
268 * that list is complete, then it is the end of this container and we need
269 * to pop back up one level for new items. If we never get an open tag for
270 * one of these things, we should assume that the container is empty and
271 * that things we find should be siblings of it. Normally, these <dl>s won't
272 * be nested so this will be 0 or 1.
274 this.containerNesting = 0;
277 * when we find a heading tag, it actually affects the title of the NEXT
278 * container in the list. This stores that heading tag and whether it was
279 * special. 'consumeHeading' resets this._
281 this.lastContainerType = Container_Normal;
284 * this contains the text from the last begin tag until now. It is reset
285 * at every begin tag. We can check it when we see a </a>, or </h3>
286 * to see what the text content of that node should be.
288 this.previousText = "";
291 * true when we hit a <dd>, which contains the description for the preceding
292 * <a> tag. We can't just check for </dd> like we can for </a> or </h3>
293 * because if there is a sub-folder, it is actually a child of the <dd>
294 * because the tag is never explicitly closed. If this is true and we see a
295 * new open tag, that means to commit the description to the previous
298 * Additional weirdness happens when the previous <dt> tag contains a <h3>:
299 * this means there is a new folder with the given description, and whose
300 * children are contained in the following <dl> list.
302 * This is handled in openContainer(), which commits previous text if
305 this.inDescription = false;
308 * contains the URL of the previous bookmark created. This is used so that
309 * when we encounter a <dd>, we know what bookmark to associate the text with.
310 * This is cleared whenever we hit a <h3>, so that we know NOT to save this
311 * with a bookmark, but to keep it until
313 this.previousLink = null;
316 * Contains a reference to the last created bookmark or folder object.
318 this.previousItem = null;
321 * Contains the date-added and last-modified-date of an imported item.
322 * Used to override the values set by insertBookmark, createFolder, etc.
324 this.previousDateAdded = null;
325 this.previousLastModifiedDate = null;
328 function BookmarkImporter(aInitialImport, aSource) {
329 this._isImportDefaults = aInitialImport;
330 this._source = aSource;
332 // This root is where we construct the bookmarks tree into, following the format
333 // of the imported file.
334 // If we're doing an initial import, the non-menu roots will be created as
335 // children of this root, so in _getBookmarkTrees we'll split them out.
336 // If we're not doing an initial import, everything gets imported under the
337 // bookmark menu folder, so there won't be any need for _getBookmarkTrees to
339 this._bookmarkTree = {
340 type: PlacesUtils.bookmarks.TYPE_FOLDER,
341 guid: PlacesUtils.bookmarks.menuGuid,
346 this._frames.push(new Frame(this._bookmarkTree));
349 BookmarkImporter.prototype = {
350 _safeTrim: function safeTrim(aStr) {
351 return aStr ? aStr.trim() : aStr;
355 return this._frames[this._frames.length - 1];
358 get _previousFrame() {
359 return this._frames[this._frames.length - 2];
363 * This is called when there is a new folder found. The folder takes the
364 * name from the previous frame's heading.
366 _newFrame: function newFrame() {
367 let frame = this._curFrame;
368 let containerTitle = frame.previousText;
369 frame.previousText = "";
370 let containerType = frame.lastContainerType;
374 type: PlacesUtils.bookmarks.TYPE_FOLDER,
377 switch (containerType) {
378 case Container_Normal:
379 // This can only be a sub-folder so no need to set a guid here.
380 folder.title = containerTitle;
382 case Container_Places:
383 folder.guid = PlacesUtils.bookmarks.rootGuid;
386 folder.guid = PlacesUtils.bookmarks.menuGuid;
388 case Container_Unfiled:
389 folder.guid = PlacesUtils.bookmarks.unfiledGuid;
391 case Container_Toolbar:
392 folder.guid = PlacesUtils.bookmarks.toolbarGuid;
396 throw new Error("Unknown bookmark container type!");
399 frame.folder.children.push(folder);
401 if (frame.previousDateAdded != null) {
402 folder.dateAdded = frame.previousDateAdded;
403 frame.previousDateAdded = null;
406 if (frame.previousLastModifiedDate != null) {
407 folder.lastModified = frame.previousLastModifiedDate;
408 frame.previousLastModifiedDate = null;
412 !folder.hasOwnProperty("dateAdded") &&
413 folder.hasOwnProperty("lastModified")
415 folder.dateAdded = folder.lastModified;
418 frame.previousItem = folder;
420 this._frames.push(new Frame(folder));
424 * Handles <hr> as a separator.
426 * @note Separators may have a title in old html files, though Places dropped
428 * We also don't import ADD_DATE or LAST_MODIFIED for separators because
429 * pre-Places bookmarks did not support them.
431 _handleSeparator: function handleSeparator() {
432 let frame = this._curFrame;
435 type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
437 frame.folder.children.push(separator);
438 frame.previousItem = separator;
442 * Called for h2,h3,h4,h5,h6. This just stores the correct information in
443 * the current frame; the actual new frame corresponding to the container
444 * associated with the heading will be created when the tag has been closed
445 * and we know the title (we don't know to create a new folder or to merge
446 * with an existing one until we have the title).
448 _handleHeadBegin: function handleHeadBegin(aElt) {
449 let frame = this._curFrame;
451 // after a heading, a previous bookmark is not applicable (for example, for
452 // the descriptions contained in a <dd>). Neither is any previous head type
453 frame.previousLink = null;
454 frame.lastContainerType = Container_Normal;
456 // It is syntactically possible for a heading to appear after another heading
457 // but before the <dl> that encloses that folder's contents. This should not
458 // happen in practice, as the file will contain "<dl></dl>" sequence for
461 // Just to be on the safe side, if we encounter
464 // <dl>...content 1...</dl>
465 // <dl>...content 2...</dl>
466 // we'll pop the stack when we find the h3 for BAR, treating that as an
467 // implicit ending of the FOO container. The output will be FOO and BAR as
468 // siblings. If there's another <dl> following (as in "content 2"), those
469 // items will be treated as further siblings of FOO and BAR
470 // This special frame popping business, of course, only happens when our
471 // frame array has more than one element so we can avoid situations where
472 // we don't have a frame to parse into anymore.
473 if (frame.containerNesting == 0 && this._frames.length > 1) {
477 // We have to check for some attributes to see if this is a "special"
478 // folder, which will have different creation rules when the end tag is
480 if (aElt.hasAttribute("personal_toolbar_folder")) {
481 if (this._isImportDefaults) {
482 frame.lastContainerType = Container_Toolbar;
484 } else if (aElt.hasAttribute("bookmarks_menu")) {
485 if (this._isImportDefaults) {
486 frame.lastContainerType = Container_Menu;
488 } else if (aElt.hasAttribute("unfiled_bookmarks_folder")) {
489 if (this._isImportDefaults) {
490 frame.lastContainerType = Container_Unfiled;
492 } else if (aElt.hasAttribute("places_root")) {
493 if (this._isImportDefaults) {
494 frame.lastContainerType = Container_Places;
497 let addDate = aElt.getAttribute("add_date");
499 frame.previousDateAdded =
500 this._convertImportedDateToInternalDate(addDate);
502 let modDate = aElt.getAttribute("last_modified");
504 frame.previousLastModifiedDate =
505 this._convertImportedDateToInternalDate(modDate);
508 this._curFrame.previousText = "";
512 * Handles "<a" tags by creating a new bookmark. The title of the bookmark
513 * will be the text content, which will be stuffed in previousText for us
514 * and which will be saved by handleLinkEnd
516 _handleLinkBegin: function handleLinkBegin(aElt) {
517 let frame = this._curFrame;
519 frame.previousItem = null;
520 frame.previousText = ""; // Will hold link text, clear it.
522 // Get the attributes we care about.
523 let href = this._safeTrim(aElt.getAttribute("href"));
524 let icon = this._safeTrim(aElt.getAttribute("icon"));
525 let iconUri = this._safeTrim(aElt.getAttribute("icon_uri"));
526 let lastCharset = this._safeTrim(aElt.getAttribute("last_charset"));
527 let keyword = this._safeTrim(aElt.getAttribute("shortcuturl"));
528 let postData = this._safeTrim(aElt.getAttribute("post_data"));
529 let dateAdded = this._safeTrim(aElt.getAttribute("add_date"));
530 let lastModified = this._safeTrim(aElt.getAttribute("last_modified"));
531 let tags = this._safeTrim(aElt.getAttribute("tags"));
533 // Ignore <a> tags that have no href.
535 frame.previousLink = Services.io.newURI(href).spec;
537 frame.previousLink = null;
543 // Only set the url for bookmarks.
544 if (frame.previousLink) {
545 bookmark.url = frame.previousLink;
549 bookmark.dateAdded = this._convertImportedDateToInternalDate(dateAdded);
551 // Save bookmark's last modified date.
553 bookmark.lastModified =
554 this._convertImportedDateToInternalDate(lastModified);
557 if (!dateAdded && lastModified) {
558 bookmark.dateAdded = bookmark.lastModified;
566 !!aTag.length && aTag.length <= PlacesUtils.bookmarks.MAX_TAG_LENGTH
569 // If we end up with none, then delete the property completely.
570 if (!bookmark.tags.length) {
571 delete bookmark.tags;
576 bookmark.charset = lastCharset;
580 bookmark.keyword = keyword;
584 bookmark.postData = postData;
588 bookmark.icon = icon;
592 bookmark.iconUri = iconUri;
595 // Add bookmark to the tree.
596 frame.folder.children.push(bookmark);
597 frame.previousItem = bookmark;
600 _handleContainerBegin: function handleContainerBegin() {
601 this._curFrame.containerNesting++;
605 * Our "indent" count has decreased, and when we hit 0 that means that this
606 * container is complete and we need to pop back to the outer frame. Never
607 * pop the toplevel frame
609 _handleContainerEnd: function handleContainerEnd() {
610 let frame = this._curFrame;
611 if (frame.containerNesting > 0) {
612 frame.containerNesting--;
614 if (this._frames.length > 1 && frame.containerNesting == 0) {
620 * Creates the new frame for this heading now that we know the name of the
621 * container (tokens since the heading open tag will have been placed in
624 _handleHeadEnd: function handleHeadEnd() {
629 * Saves the title for the given bookmark.
631 _handleLinkEnd: function handleLinkEnd() {
632 let frame = this._curFrame;
633 frame.previousText = frame.previousText.trim();
635 if (frame.previousItem != null) {
636 frame.previousItem.title = frame.previousText;
639 frame.previousText = "";
642 _openContainer: function openContainer(aElt) {
643 if (aElt.namespaceURI != "http://www.w3.org/1999/xhtml") {
646 switch (aElt.localName) {
652 this._handleHeadBegin(aElt);
655 this._handleLinkBegin(aElt);
660 this._handleContainerBegin();
663 this._curFrame.inDescription = true;
666 this._handleSeparator(aElt);
671 _closeContainer: function closeContainer(aElt) {
672 let frame = this._curFrame;
674 // Although we no longer support importing descriptions, we still need to
675 // clear any previous text, so that it doesn't get swallowed into other elements.
676 if (frame.inDescription) {
677 frame.previousText = "";
678 frame.inDescription = false;
681 if (aElt.namespaceURI != "http://www.w3.org/1999/xhtml") {
684 switch (aElt.localName) {
688 this._handleContainerEnd();
700 this._handleHeadEnd();
703 this._handleLinkEnd();
710 _appendText: function appendText(str) {
711 this._curFrame.previousText += str;
715 * Converts a string date in seconds to a date object
717 _convertImportedDateToInternalDate:
718 function convertImportedDateToInternalDate(aDate) {
720 if (aDate && !isNaN(aDate)) {
721 return new Date(parseInt(aDate) * 1000); // in bookmarks.html this value is in seconds
729 _walkTreeForImport(aDoc) {
737 switch (current.nodeType) {
738 case current.ELEMENT_NODE:
739 this._openContainer(current);
741 case current.TEXT_NODE:
742 this._appendText(current.data);
745 if ((next = current.firstChild)) {
750 if (current.nodeType == current.ELEMENT_NODE) {
751 this._closeContainer(current);
753 if (current == aDoc) {
756 if ((next = current.nextSibling)) {
760 current = current.parentNode;
766 * Returns the bookmark tree(s) from the importer. These are suitable for
767 * passing to PlacesUtils.bookmarks.insertTree().
769 * @returns {Array} An array of bookmark trees.
771 _getBookmarkTrees() {
772 // If we're not importing defaults, then everything gets imported under the
774 if (!this._isImportDefaults) {
775 return [this._bookmarkTree];
778 // If we are importing defaults, we need to separate out the top-level
779 // default folders into separate items, for the caller to pass into insertTree.
780 let bookmarkTrees = [this._bookmarkTree];
782 // The children of this "root" element will contain normal children of the
783 // bookmark menu as well as the places roots. Hence, we need to filter out
784 // the separate roots, but keep the children that are relevant to the
786 this._bookmarkTree.children = this._bookmarkTree.children.filter(child => {
789 PlacesUtils.bookmarks.userContentRoots.includes(child.guid)
791 bookmarkTrees.push(child);
797 return bookmarkTrees;
801 * Imports the bookmarks from the importer into the places database.
803 * @param {BookmarkImporter} importer The importer from which to get the
804 * bookmark information.
805 * @returns {number} The number of imported bookmarks, not including
806 * folders and separators
808 async _importBookmarks() {
809 if (this._isImportDefaults) {
810 await PlacesUtils.bookmarks.eraseEverything();
813 let bookmarksTrees = this._getBookmarkTrees();
814 let bookmarkCount = 0;
815 for (let tree of bookmarksTrees) {
816 if (!tree.children.length) {
820 // Give the tree the source.
821 tree.source = this._source;
822 let bookmarks = await PlacesUtils.bookmarks.insertTree(tree, {
823 fixupOrSkipInvalidEntries: true,
825 // We want to count only bookmarks, not folders or separators
826 bookmarkCount += bookmarks.filter(
827 bookmark => bookmark.type == PlacesUtils.bookmarks.TYPE_BOOKMARK
829 insertFaviconsForTree(tree);
831 return bookmarkCount;
835 * Imports data into the places database from the supplied url.
837 * @param {String} href The url to import data from.
838 * @returns {number} The number of imported bookmarks, not including
839 * folders and separators.
841 async importFromURL(href) {
842 let data = await fetchData(href);
844 if (this._isImportDefaults && data) {
845 // Localize default bookmarks. Find rel="localization" links and manually
846 // localize using them.
848 let links = data.head.querySelectorAll("link[rel='localization']");
849 for (let link of links) {
850 if (link.getAttribute("href")) {
851 // We need the text, not the fully qualified URL, so we use `getAttribute`.
852 hrefs.push(link.getAttribute("href"));
857 let domLoc = new DOMLocalization(hrefs);
858 await domLoc.translateFragment(data.body);
862 this._walkTreeForImport(data);
863 return this._importBookmarks();
867 function BookmarkExporter(aBookmarksTree) {
868 // Create a map of the roots.
869 let rootsMap = new Map();
870 for (let child of aBookmarksTree.children) {
872 rootsMap.set(child.root, child);
873 // Also take the opportunity to get the correctly localised title for the
875 child.title = PlacesUtils.bookmarks.getLocalizedTitle(child);
879 // For backwards compatibility reasons the bookmarks menu is the root, while
880 // the bookmarks toolbar and unfiled bookmarks will be child items.
881 this._root = rootsMap.get("bookmarksMenuFolder");
883 for (let key of ["toolbarFolder", "unfiledBookmarksFolder"]) {
884 let root = rootsMap.get(key);
885 if (root.children && root.children.length) {
886 if (!this._root.children) {
887 this._root.children = [];
889 this._root.children.push(root);
894 BookmarkExporter.prototype = {
895 exportToFile: function exportToFile(aFilePath) {
896 return (async () => {
897 // Create a file that can be accessed by the current user only.
898 let out = FileUtils.openAtomicFileOutputStream(
899 new FileUtils.File(aFilePath)
902 // We need a buffered output stream for performance. See bug 202477.
903 let bufferedOut = Cc[
904 "@mozilla.org/network/buffered-output-stream;1"
905 ].createInstance(Ci.nsIBufferedOutputStream);
906 bufferedOut.init(out, 4096);
908 // Write bookmarks in UTF-8.
909 this._converterOut = Cc[
910 "@mozilla.org/intl/converter-output-stream;1"
911 ].createInstance(Ci.nsIConverterOutputStream);
912 this._converterOut.init(bufferedOut, "utf-8");
915 await this._writeContainer(this._root);
916 // Retain the target file on success only.
917 bufferedOut.QueryInterface(Ci.nsISafeOutputStream).finish();
919 this._converterOut.close();
920 this._converterOut = null;
934 this._converterOut.writeString(aText || "");
937 _writeAttribute(aName, aValue) {
938 this._write(" " + aName + '="' + aValue + '"');
942 this._write(aText + "\n");
946 this._writeLine("<!DOCTYPE NETSCAPE-Bookmark-file-1>");
947 this._writeLine("<!-- This is an automatically generated file.");
948 this._writeLine(" It will be read and overwritten.");
949 this._writeLine(" DO NOT EDIT! -->");
951 '<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">'
953 this._writeLine(`<meta http-equiv="Content-Security-Policy"
954 content="default-src 'self'; script-src 'none'; img-src data: *; object-src 'none'"></meta>`);
955 this._writeLine("<TITLE>Bookmarks</TITLE>");
958 async _writeContainer(aItem, aIndent = "") {
959 if (aItem == this._root) {
960 this._writeLine("<H1>" + escapeHtmlEntities(this._root.title) + "</H1>");
963 this._write(aIndent + "<DT><H3");
964 this._writeDateAttributes(aItem);
966 if (aItem.root === "toolbarFolder") {
967 this._writeAttribute("PERSONAL_TOOLBAR_FOLDER", "true");
968 } else if (aItem.root === "unfiledBookmarksFolder") {
969 this._writeAttribute("UNFILED_BOOKMARKS_FOLDER", "true");
971 this._writeLine(">" + escapeHtmlEntities(aItem.title) + "</H3>");
974 this._writeLine(aIndent + "<DL><p>");
975 if (aItem.children) {
976 await this._writeContainerContents(aItem, aIndent);
978 if (aItem == this._root) {
979 this._writeLine(aIndent + "</DL>");
981 this._writeLine(aIndent + "</DL><p>");
985 async _writeContainerContents(aItem, aIndent) {
986 let localIndent = aIndent + EXPORT_INDENT;
988 for (let child of aItem.children) {
989 if (child.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
990 await this._writeContainer(child, localIndent);
991 } else if (child.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) {
992 this._writeSeparator(child, localIndent);
994 await this._writeItem(child, localIndent);
999 _writeSeparator(aItem, aIndent) {
1000 this._write(aIndent + "<HR");
1001 // We keep exporting separator titles, but don't support them anymore.
1003 this._writeAttribute("NAME", escapeHtmlEntities(aItem.title));
1008 async _writeItem(aItem, aIndent) {
1010 NetUtil.newURI(aItem.uri);
1012 // If the item URI is invalid, skip the item instead of failing later.
1016 this._write(aIndent + "<DT><A");
1017 this._writeAttribute("HREF", escapeUrl(aItem.uri));
1018 this._writeDateAttributes(aItem);
1019 await this._writeFaviconAttribute(aItem);
1021 if (aItem.keyword) {
1022 this._writeAttribute("SHORTCUTURL", escapeHtmlEntities(aItem.keyword));
1023 if (aItem.postData) {
1024 this._writeAttribute("POST_DATA", escapeHtmlEntities(aItem.postData));
1028 if (aItem.charset) {
1029 this._writeAttribute("LAST_CHARSET", escapeHtmlEntities(aItem.charset));
1032 this._writeAttribute("TAGS", escapeHtmlEntities(aItem.tags));
1034 this._writeLine(">" + escapeHtmlEntities(aItem.title) + "</A>");
1037 _writeDateAttributes(aItem) {
1038 if (aItem.dateAdded) {
1039 this._writeAttribute(
1041 Math.floor(aItem.dateAdded / MICROSEC_PER_SEC)
1044 if (aItem.lastModified) {
1045 this._writeAttribute(
1047 Math.floor(aItem.lastModified / MICROSEC_PER_SEC)
1052 async _writeFaviconAttribute(aItem) {
1053 if (!aItem.iconUri) {
1058 favicon = await PlacesUtils.promiseFaviconData(aItem.uri);
1060 console.error("Unexpected Error trying to fetch icon data");
1064 this._writeAttribute("ICON_URI", escapeUrl(favicon.uri.spec));
1066 if (!favicon.uri.schemeIs("chrome") && favicon.dataLen > 0) {
1067 let faviconContents =
1068 "data:image/png;base64," +
1069 base64EncodeString(String.fromCharCode.apply(String, favicon.data));
1070 this._writeAttribute("ICON", faviconContents);
1076 * Handles inserting favicons into the database for a bookmark node.
1077 * It is assumed the node has already been inserted into the bookmarks
1080 * @param {Object} node The bookmark node for icons to be inserted.
1082 function insertFaviconForNode(node) {
1085 PlacesUtils.favicons.setFaviconForPage(
1086 Services.io.newURI(node.url),
1087 // Create a fake favicon URI to use (FIXME: bug 523932)
1088 Services.io.newURI("fake-favicon-uri:" + node.url),
1089 Services.io.newURI(node.icon)
1092 console.error("Failed to import favicon data:", ex);
1096 if (!node.iconUri) {
1101 PlacesUtils.favicons.setAndFetchFaviconForPage(
1102 Services.io.newURI(node.url),
1103 Services.io.newURI(node.iconUri),
1105 PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
1107 Services.scriptSecurityManager.getSystemPrincipal()
1110 console.error("Failed to import favicon URI:" + ex);
1115 * Handles inserting favicons into the database for a bookmark tree - a node
1118 * It is assumed the nodes have already been inserted into the bookmarks
1121 * @param {Object} nodeTree The bookmark node tree for icons to be inserted.
1123 function insertFaviconsForTree(nodeTree) {
1124 insertFaviconForNode(nodeTree);
1126 if (nodeTree.children) {
1127 for (let child of nodeTree.children) {
1128 insertFaviconsForTree(child);
1134 * Handles fetching data from a URL.
1136 * @param {String} href The url to fetch data from.
1137 * @return {Promise} Returns a promise that is resolved with the data once
1138 * the fetch is complete, or is rejected if it fails.
1140 function fetchData(href) {
1141 return new Promise((resolve, reject) => {
1142 let xhr = new XMLHttpRequest();
1143 xhr.onload = () => {
1144 resolve(xhr.responseXML);
1150 reject(new Error("xmlhttprequest failed"));
1152 xhr.open("GET", href);
1153 xhr.responseType = "document";
1154 xhr.overrideMimeType("text/html");