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