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 59var EXPORTED_SYMBOLS = ["BookmarkHTMLUtils"]; 60 61const { XPCOMUtils } = ChromeUtils.import( 62 "resource://gre/modules/XPCOMUtils.jsm" 63); 64const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 65const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); 66const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); 67const { FileUtils } = ChromeUtils.import( 68 "resource://gre/modules/FileUtils.jsm" 69); 70const { PlacesUtils } = ChromeUtils.import( 71 "resource://gre/modules/PlacesUtils.jsm" 72); 73 74XPCOMUtils.defineLazyGlobalGetters(this, ["XMLHttpRequest"]); 75 76ChromeUtils.defineModuleGetter( 77 this, 78 "PlacesBackups", 79 "resource://gre/modules/PlacesBackups.jsm" 80); 81 82const Container_Normal = 0; 83const Container_Toolbar = 1; 84const Container_Menu = 2; 85const Container_Unfiled = 3; 86const Container_Places = 4; 87 88const MICROSEC_PER_SEC = 1000000; 89 90const EXPORT_INDENT = " "; // four spaces 91 92function 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 */ 107function escapeHtmlEntities(aText) { 108 return (aText || "") 109 .replace(/&/g, "&") 110 .replace(/</g, "<") 111 .replace(/>/g, ">") 112 .replace(/"/g, """) 113 .replace(/'/g, "'"); 114} 115 116/** 117 * Provides URL escaping for use in HTML attributes of the bookmarks file, 118 * compatible with the old bookmarks system. 119 */ 120function escapeUrl(aText) { 121 return (aText || "").replace(/"/g, "%22"); 122} 123 124function notifyObservers(aTopic, aInitialImport) { 125 Services.obs.notifyObservers( 126 null, 127 aTopic, 128 aInitialImport ? "html-initial" : "html" 129 ); 130} 131 132var 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 268function 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 334function 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 355BookmarkImporter.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 848function 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 875BookmarkExporter.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 */ 1061function 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 */ 1112function 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 */ 1129function 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} 1144