DXR is a code search and navigation tool aimed at making sense of large projects. It supports full-text and regex searches as well as structural queries.

Mercurial (31ec81b5d7bb)

VCS Links

Line Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

this.EXPORTED_SYMBOLS = [ "BookmarkJSONUtils" ];

const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;

Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");

this.BookmarkJSONUtils = Object.freeze({
  /**
   * Import bookmarks from a url.
   *
   * @param aURL
   *        url of the bookmark data.
   * @param aReplace
   *        Boolean if true, replace existing bookmarks, else merge.
   *
   * @return {Promise}
   * @resolves When the new bookmarks have been created.
   * @rejects JavaScript exception.
   */
  importFromURL: function BJU_importFromURL(aURL, aReplace) {
    let importer = new BookmarkImporter();
    return importer.importFromURL(aURL, aReplace);
  },

  /**
   * Restores bookmarks and tags from a JSON file.
   * @note any item annotated with "places/excludeFromBackup" won't be removed
   *       before executing the restore.
   *
   * @param aFile
   *        nsIFile of bookmarks in JSON format to be restored.
   * @param aReplace
   *        Boolean if true, replace existing bookmarks, else merge.
   *
   * @return {Promise}
   * @resolves When the new bookmarks have been created.
   * @rejects JavaScript exception.
   */
  importFromFile: function BJU_importFromFile(aFile, aReplace) {
    let importer = new BookmarkImporter();
    return importer.importFromFile(aFile, aReplace);
  },

  /**
   * Serializes bookmarks using JSON, and writes to the supplied file.
   *
   * @param aLocalFile
   *        nsIFile for the "bookmarks.json" file to be created.
   *
   * @return {Promise}
   * @resolves When the file has been created.
   * @rejects JavaScript exception.
   */
  exportToFile: function BJU_exportToFile(aLocalFile) {
    let exporter = new BookmarkExporter();
    return exporter.exportToFile(aLocalFile);
  },

  /**
   * Takes a JSON-serialized node and inserts it into the db.
   *
   * @param aData
   *        The unwrapped data blob of dropped or pasted data.
   * @param aContainer
   *        The container the data was dropped or pasted into
   * @param aIndex
   *        The index within the container the item was dropped or pasted at
   * @return {Promise}
   * @resolves an array containing of maps of old folder ids to new folder ids,
   *           and an array of saved search ids that need to be fixed up.
   *           eg: [[[oldFolder1, newFolder1]], [search1]]
   * @rejects JavaScript exception.
   */
  importJSONNode: function BJU_importJSONNode(aData, aContainer, aIndex,
                                              aGrandParentId) {
    let importer = new BookmarkImporter();
    // Can't make importJSONNode a task until we have made the
    // runInBatchMode in BI_importFromJSON to run asynchronously. Bug 890203
    return Promise.resolve(importer.importJSONNode(aData, aContainer, aIndex, aGrandParentId));
  },

  /**
   * Serializes the given node (and all its descendents) as JSON
   * and writes the serialization to the given output stream.
   *
   * @param   aNode
   *          An nsINavHistoryResultNode
   * @param   aStream
   *          An nsIOutputStream. NOTE: it only uses the write(str, len)
   *          method of nsIOutputStream. The caller is responsible for
   *          closing the stream.
   * @param   aIsUICommand
   *          Boolean - If true, modifies serialization so that each node self-contained.
   *          For Example, tags are serialized inline with each bookmark.
   * @param   aResolveShortcuts
   *          Converts folder shortcuts into actual folders.
   * @param   aExcludeItems
   *          An array of item ids that should not be written to the backup.
   * @return {Promise}
   * @resolves When node have been serialized and wrote to output stream.
   * @rejects JavaScript exception.
   */
  serializeNodeAsJSONToOutputStream: function BJU_serializeNodeAsJSONToOutputStream(
    aNode, aStream, aIsUICommand, aResolveShortcuts, aExcludeItems) {
    let deferred = Promise.defer();
    Services.tm.mainThread.dispatch(function() {
      try {
        BookmarkNode.serializeAsJSONToOutputStream(
          aNode, aStream, aIsUICommand, aResolveShortcuts, aExcludeItems);
        deferred.resolve();
      } catch (ex) {
        deferred.reject(ex);
      }
    }, Ci.nsIThread.DISPATCH_NORMAL);
    return deferred.promise;
  }
});

function BookmarkImporter() {}
BookmarkImporter.prototype = {
  /**
   * Import bookmarks from a file.
   *
   * @param aFile
   *        the bookmark file.
   * @param aReplace
   *        Boolean if true, replace existing bookmarks, else merge.
   *
   * @return {Promise}
   * @resolves When the new bookmarks have been created.
   * @rejects JavaScript exception.
   */
  importFromFile: function(aFile, aReplace) {
    if (aFile.exists()) {
      return this.importFromURL(NetUtil.newURI(aFile).spec, aReplace);
    }

    notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN);

    return Task.spawn(function() {
      notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED);
      throw new Error("File does not exist.");
    });
  },

  /**
   * Import bookmarks from a url.
   *
   * @param aURL
   *        url of the bookmark data.
   * @param aReplace
   *        Boolean if true, replace existing bookmarks, else merge.
   *
   * @return {Promise}
   * @resolves When the new bookmarks have been created.
   * @rejects JavaScript exception.
   */
  importFromURL: function BI_importFromURL(aURL, aReplace) {
    let deferred = Promise.defer();
    notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN);

    let streamObserver = {
      onStreamComplete: function (aLoader, aContext, aStatus, aLength,
                                  aResult) {
        let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
                        createInstance(Ci.nsIScriptableUnicodeConverter);
        converter.charset = "UTF-8";

        Task.spawn(function() {
          try {
            let jsonString =
              converter.convertFromByteArray(aResult, aResult.length);
            yield this.importFromJSON(jsonString, aReplace);
            notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS);
            deferred.resolve();
          } catch (ex) {
            notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED);
            Cu.reportError("Failed to import from URL: " + ex);
            deferred.reject(ex);
          }
        }.bind(this));
      }.bind(this)
    };

    try {
      let channel = Services.io.newChannelFromURI(NetUtil.newURI(aURL));
      let streamLoader = Cc["@mozilla.org/network/stream-loader;1"].
                         createInstance(Ci.nsIStreamLoader);

      streamLoader.init(streamObserver);
      channel.asyncOpen(streamLoader, channel);
    } catch (ex) {
      notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED);
      Cu.reportError("Failed to import from URL: " + ex);
      deferred.reject(ex);
    }

    return deferred.promise;
  },

  /**
   * Import bookmarks from a JSON string.
   *
   * @param aString
   *        JSON string of serialized bookmark data.
   * @param aReplace
   *        Boolean if true, replace existing bookmarks, else merge.
   */
  importFromJSON: function BI_importFromJSON(aString, aReplace) {
    let deferred = Promise.defer();
    let nodes =
      PlacesUtils.unwrapNodes(aString, PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER);

    if (nodes.length == 0 || !nodes[0].children ||
        nodes[0].children.length == 0) {
      Services.tm.mainThread.dispatch(function() {
        deferred.resolve(); // Nothing to restore
      }, Ci.nsIThread.DISPATCH_NORMAL);
    } else {
      // Ensure tag folder gets processed last
      nodes[0].children.sort(function sortRoots(aNode, bNode) {
        return (aNode.root && aNode.root == "tagsFolder") ? 1 :
               (bNode.root && bNode.root == "tagsFolder") ? -1 : 0;
      });

      let batch = {
        nodes: nodes[0].children,
        runBatched: function runBatched() {
          if (aReplace) {
            // Get roots excluded from the backup, we will not remove them
            // before restoring.
            let excludeItems = PlacesUtils.annotations.getItemsWithAnnotation(
                                 PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
            // Delete existing children of the root node, excepting:
            // 1. special folders: delete the child nodes
            // 2. tags folder: untag via the tagging api
            let root = PlacesUtils.getFolderContents(PlacesUtils.placesRootId,
                                                   false, false).root;
            let childIds = [];
            for (let i = 0; i < root.childCount; i++) {
              let childId = root.getChild(i).itemId;
              if (excludeItems.indexOf(childId) == -1 &&
                  childId != PlacesUtils.tagsFolderId) {
                childIds.push(childId);
              }
            }
            root.containerOpen = false;

            for (let i = 0; i < childIds.length; i++) {
              let rootItemId = childIds[i];
              if (PlacesUtils.isRootItem(rootItemId)) {
                PlacesUtils.bookmarks.removeFolderChildren(rootItemId);
              } else {
                PlacesUtils.bookmarks.removeItem(rootItemId);
              }
            }
          }

          let searchIds = [];
          let folderIdMap = [];

          for (let node of batch.nodes) {
            if (!node.children || node.children.length == 0)
              continue; // Nothing to restore for this root

            if (node.root) {
              let container = PlacesUtils.placesRootId; // Default to places root
              switch (node.root) {
                case "bookmarksMenuFolder":
                  container = PlacesUtils.bookmarksMenuFolderId;
                  break;
                case "tagsFolder":
                  container = PlacesUtils.tagsFolderId;
                  break;
                case "unfiledBookmarksFolder":
                  container = PlacesUtils.unfiledBookmarksFolderId;
                  break;
                case "toolbarFolder":
                  container = PlacesUtils.toolbarFolderId;
                  break;
              }

              // Insert the data into the db
              for (let child of node.children) {
                let index = child.index;
                let [folders, searches] =
                  this.importJSONNode(child, container, index, 0);
                for (let i = 0; i < folders.length; i++) {
                  if (folders[i])
                    folderIdMap[i] = folders[i];
                }
                searchIds = searchIds.concat(searches);
              }
            } else {
              this.importJSONNode(
                node, PlacesUtils.placesRootId, node.index, 0);
            }
          }

          // Fixup imported place: uris that contain folders
          searchIds.forEach(function(aId) {
            let oldURI = PlacesUtils.bookmarks.getBookmarkURI(aId);
            let uri = fixupQuery(oldURI, folderIdMap);
            if (!uri.equals(oldURI)) {
              PlacesUtils.bookmarks.changeBookmarkURI(aId, uri);
            }
          });

          deferred.resolve();
        }.bind(this)
      };

      PlacesUtils.bookmarks.runInBatchMode(batch, null);
    }
    return deferred.promise;
  },

  /**
   * Takes a JSON-serialized node and inserts it into the db.
   *
   * @param aData
   *        The unwrapped data blob of dropped or pasted data.
   * @param aContainer
   *        The container the data was dropped or pasted into
   * @param aIndex
   *        The index within the container the item was dropped or pasted at
   * @return an array containing of maps of old folder ids to new folder ids,
   *         and an array of saved search ids that need to be fixed up.
   *         eg: [[[oldFolder1, newFolder1]], [search1]]
   */
  importJSONNode: function BI_importJSONNode(aData, aContainer, aIndex,
                                             aGrandParentId) {
    let folderIdMap = [];
    let searchIds = [];
    let id = -1;
    switch (aData.type) {
      case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER:
        if (aContainer == PlacesUtils.tagsFolderId) {
          // Node is a tag
          if (aData.children) {
            aData.children.forEach(function(aChild) {
              try {
                PlacesUtils.tagging.tagURI(
                  NetUtil.newURI(aChild.uri), [aData.title]);
              } catch (ex) {
                // Invalid tag child, skip it
              }
            });
            return [folderIdMap, searchIds];
          }
        } else if (aData.livemark && aData.annos) {
          // Node is a livemark
          let feedURI = null;
          let siteURI = null;
          aData.annos = aData.annos.filter(function(aAnno) {
            switch (aAnno.name) {
              case PlacesUtils.LMANNO_FEEDURI:
                feedURI = NetUtil.newURI(aAnno.value);
                return false;
              case PlacesUtils.LMANNO_SITEURI:
                siteURI = NetUtil.newURI(aAnno.value);
                return false;
              default:
                return true;
            }
          });

          if (feedURI) {
            PlacesUtils.livemarks.addLivemark({
              title: aData.title,
              feedURI: feedURI,
              parentId: aContainer,
              index: aIndex,
              lastModified: aData.lastModified,
              siteURI: siteURI
            }, function(aStatus, aLivemark) {
              if (Components.isSuccessCode(aStatus)) {
                let id = aLivemark.id;
                if (aData.dateAdded)
                  PlacesUtils.bookmarks.setItemDateAdded(id, aData.dateAdded);
                if (aData.annos && aData.annos.length)
                  PlacesUtils.setAnnotationsForItem(id, aData.annos);
              }
            });
          }
        } else {
          id = PlacesUtils.bookmarks.createFolder(
                 aContainer, aData.title, aIndex);
          folderIdMap[aData.id] = id;
          // Process children
          if (aData.children) {
            for (let i = 0; i < aData.children.length; i++) {
              let child = aData.children[i];
              let [folders, searches] =
                this.importJSONNode(child, id, i, aContainer);
              for (let j = 0; j < folders.length; j++) {
                if (folders[j])
                  folderIdMap[j] = folders[j];
              }
              searchIds = searchIds.concat(searches);
            }
          }
        }
        break;
      case PlacesUtils.TYPE_X_MOZ_PLACE:
        id = PlacesUtils.bookmarks.insertBookmark(
               aContainer, NetUtil.newURI(aData.uri), aIndex, aData.title);
        if (aData.keyword)
          PlacesUtils.bookmarks.setKeywordForBookmark(id, aData.keyword);
        if (aData.tags) {
          let tags = aData.tags.split(", ");
          if (tags.length)
            PlacesUtils.tagging.tagURI(NetUtil.newURI(aData.uri), tags);
        }
        if (aData.charset) {
          PlacesUtils.annotations.setPageAnnotation(
            NetUtil.newURI(aData.uri), PlacesUtils.CHARSET_ANNO, aData.charset,
            0, Ci.nsIAnnotationService.EXPIRE_NEVER);
        }
        if (aData.uri.substr(0, 6) == "place:")
          searchIds.push(id);
        if (aData.icon) {
          try {
            // Create a fake faviconURI to use (FIXME: bug 523932)
            let faviconURI = NetUtil.newURI("fake-favicon-uri:" + aData.uri);
            PlacesUtils.favicons.replaceFaviconDataFromDataURL(
              faviconURI, aData.icon, 0);
            PlacesUtils.favicons.setAndFetchFaviconForPage(
              NetUtil.newURI(aData.uri), faviconURI, false,
              PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE);
          } catch (ex) {
            Components.utils.reportError("Failed to import favicon data:" + ex);
          }
        }
        if (aData.iconUri) {
          try {
            PlacesUtils.favicons.setAndFetchFaviconForPage(
              NetUtil.newURI(aData.uri), NetUtil.newURI(aData.iconUri), false,
              PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE);
          } catch (ex) {
            Components.utils.reportError("Failed to import favicon URI:" + ex);
          }
        }
        break;
      case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR:
        id = PlacesUtils.bookmarks.insertSeparator(aContainer, aIndex);
        break;
      default:
        // Unknown node type
    }

    // Set generic properties, valid for all nodes
    if (id != -1 && aContainer != PlacesUtils.tagsFolderId &&
        aGrandParentId != PlacesUtils.tagsFolderId) {
      if (aData.dateAdded)
        PlacesUtils.bookmarks.setItemDateAdded(id, aData.dateAdded);
      if (aData.lastModified)
        PlacesUtils.bookmarks.setItemLastModified(id, aData.lastModified);
      if (aData.annos && aData.annos.length)
        PlacesUtils.setAnnotationsForItem(id, aData.annos);
    }

    return [folderIdMap, searchIds];
  }
}

function notifyObservers(topic) {
  Services.obs.notifyObservers(null, topic, "json");
}

/**
 * Replaces imported folder ids with their local counterparts in a place: URI.
 *
 * @param   aURI
 *          A place: URI with folder ids.
 * @param   aFolderIdMap
 *          An array mapping old folder id to new folder ids.
 * @returns the fixed up URI if all matched. If some matched, it returns
 *          the URI with only the matching folders included. If none matched
 *          it returns the input URI unchanged.
 */
function fixupQuery(aQueryURI, aFolderIdMap) {
  let convert = function(str, p1, offset, s) {
    return "folder=" + aFolderIdMap[p1];
  }
  let stringURI = aQueryURI.spec.replace(/folder=([0-9]+)/g, convert);

  return NetUtil.newURI(stringURI);
}

function BookmarkExporter() {}
BookmarkExporter.prototype = {
  exportToFile: function BE_exportToFile(aLocalFile) {
    return Task.spawn(this._writeToFile(aLocalFile));
  },

  _converterOut: null,

  _writeToFile: function BE__writeToFile(aLocalFile) {
    // Create a file that can be accessed by the current user only.
    let safeFileOut = Cc["@mozilla.org/network/safe-file-output-stream;1"].
                      createInstance(Ci.nsIFileOutputStream);
    safeFileOut.init(aLocalFile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
                     FileUtils.MODE_TRUNCATE, parseInt("0600", 8), 0);
    let nodeCount;

    try {
      // We need a buffered output stream for performance.  See bug 202477.
      let bufferedOut = Cc["@mozilla.org/network/buffered-output-stream;1"].
                        createInstance(Ci.nsIBufferedOutputStream);
      bufferedOut.init(safeFileOut, 4096);
      try {
        // Write bookmarks in UTF-8.
        this._converterOut = Cc["@mozilla.org/intl/converter-output-stream;1"].
                             createInstance(Ci.nsIConverterOutputStream);
        this._converterOut.init(bufferedOut, "utf-8", 0, 0);
        try {
          nodeCount = yield this._writeContentToFile();

          // Flush the buffer and retain the target file on success only.
          bufferedOut.QueryInterface(Ci.nsISafeOutputStream).finish();
        } finally {
          this._converterOut.close();
          this._converterOut = null;
        }
      } finally {
        bufferedOut.close();
      }
    } finally {
      safeFileOut.close();
    }
    throw new Task.Result(nodeCount);
  },

  _writeContentToFile: function BE__writeContentToFile() {
    return Task.spawn(function() {
      // Weep over stream interface variance.
      let streamProxy = {
        converter: this._converterOut,
        write: function(aData, aLen) {
          this.converter.writeString(aData);
        }
      };

      // Get list of itemIds that must be excluded from the backup.
      let excludeItems = PlacesUtils.annotations.getItemsWithAnnotation(
                           PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
      let root = PlacesUtils.getFolderContents(PlacesUtils.placesRootId, false,
                                               false).root;
      // Serialize to JSON and write to stream.
      let nodeCount = yield BookmarkNode.serializeAsJSONToOutputStream(root,
                                                                       streamProxy,
                                                                       false,
                                                                       false,
                                                                       excludeItems);
      root.containerOpen = false;

      throw new Task.Result(nodeCount);
    }.bind(this));
  }
}

let BookmarkNode = {
  /**
   * Serializes the given node (and all its descendents) as JSON
   * and writes the serialization to the given output stream.
   *
   * @param   aNode
   *          An nsINavHistoryResultNode
   * @param   aStream
   *          An nsIOutputStream. NOTE: it only uses the write(str, len)
   *          method of nsIOutputStream. The caller is responsible for
   *          closing the stream.
   * @param   aIsUICommand
   *          Boolean - If true, modifies serialization so that each node self-contained.
   *          For Example, tags are serialized inline with each bookmark.
   * @param   aResolveShortcuts
   *          Converts folder shortcuts into actual folders.
   * @param   aExcludeItems
   *          An array of item ids that should not be written to the backup.
   * @returns Task promise
   * @resolves the number of serialized uri nodes.
   */
  serializeAsJSONToOutputStream: function BN_serializeAsJSONToOutputStream(
    aNode, aStream, aIsUICommand, aResolveShortcuts, aExcludeItems) {

    return Task.spawn(function() {
      // Serialize to stream
      let array = [];
      let result = yield this._appendConvertedNode(aNode, null, array,
                                                   aIsUICommand,
                                                   aResolveShortcuts,
                                                   aExcludeItems);
      if (result.appendedNode) {
        let json = JSON.stringify(array[0]);
        aStream.write(json, json.length);
      } else {
        throw Cr.NS_ERROR_UNEXPECTED;
      }
      throw new Task.Result(result.nodeCount);
    }.bind(this));
  },

  _appendConvertedNode: function BN__appendConvertedNode(
    bNode, aIndex, aArray, aIsUICommand, aResolveShortcuts, aExcludeItems) {
    return Task.spawn(function() {
      let node = {};
      let nodeCount = 0;

      // Set index in order received
      // XXX handy shortcut, but are there cases where we don't want
      // to export using the sorting provided by the query?
      if (aIndex)
        node.index = aIndex;

      this._addGenericProperties(bNode, node, aResolveShortcuts);

      let parent = bNode.parent;
      let grandParent = parent ? parent.parent : null;

      if (PlacesUtils.nodeIsURI(bNode)) {
        // Tag root accept only folder nodes
        if (parent && parent.itemId == PlacesUtils.tagsFolderId)
          throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });

        // Check for url validity, since we can't halt while writing a backup.
        // This will throw if we try to serialize an invalid url and it does
        // not make sense saving a wrong or corrupt uri node.
        try {
          NetUtil.newURI(bNode.uri);
        } catch (ex) {
          throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
        }

        yield this._addURIProperties(bNode, node, aIsUICommand);
        nodeCount++;
      } else if (PlacesUtils.nodeIsContainer(bNode)) {
        // Tag containers accept only uri nodes
        if (grandParent && grandParent.itemId == PlacesUtils.tagsFolderId)
          throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });

        this._addContainerProperties(bNode, node, aIsUICommand,
                                     aResolveShortcuts);
      } else if (PlacesUtils.nodeIsSeparator(bNode)) {
        // Tag root accept only folder nodes
        // Tag containers accept only uri nodes
        if ((parent && parent.itemId == PlacesUtils.tagsFolderId) ||
            (grandParent && grandParent.itemId == PlacesUtils.tagsFolderId))
          throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });

        this._addSeparatorProperties(bNode, node);
      }

      if (!node.feedURI && node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
        nodeCount += yield this._appendConvertedComplexNode(node,
                                                           bNode,
                                                           aArray,
                                                           aIsUICommand,
                                                           aResolveShortcuts,
                                                           aExcludeItems)
        throw new Task.Result({ appendedNode: true, nodeCount: nodeCount });
      }

      aArray.push(node);
      throw new Task.Result({ appendedNode: true, nodeCount: nodeCount });
    }.bind(this));
  },

  _addGenericProperties: function BN__addGenericProperties(
    aPlacesNode, aJSNode, aResolveShortcuts) {
    aJSNode.title = aPlacesNode.title;
    aJSNode.id = aPlacesNode.itemId;
    if (aJSNode.id != -1) {
      let parent = aPlacesNode.parent;
      if (parent)
        aJSNode.parent = parent.itemId;
      let dateAdded = aPlacesNode.dateAdded;
      if (dateAdded)
        aJSNode.dateAdded = dateAdded;
      let lastModified = aPlacesNode.lastModified;
      if (lastModified)
        aJSNode.lastModified = lastModified;

      // XXX need a hasAnnos api
      let annos = [];
      try {
        annos =
          PlacesUtils.getAnnotationsForItem(aJSNode.id).filter(function(anno) {
          // XXX should whitelist this instead, w/ a pref for
          // backup/restore of non-whitelisted annos
          // XXX causes JSON encoding errors, so utf-8 encode
          // anno.value = unescape(encodeURIComponent(anno.value));
          if (anno.name == PlacesUtils.LMANNO_FEEDURI)
            aJSNode.livemark = 1;
          if (anno.name == PlacesUtils.READ_ONLY_ANNO && aResolveShortcuts) {
            // When copying a read-only node, remove the read-only annotation.
            return false;
          }
          return true;
        });
      } catch(ex) {}
      if (annos.length != 0)
        aJSNode.annos = annos;
    }
    // XXXdietrich - store annos for non-bookmark items
  },

  _addURIProperties: function BN__addURIProperties(
    aPlacesNode, aJSNode, aIsUICommand) {
    return Task.spawn(function() {
      aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE;
      aJSNode.uri = aPlacesNode.uri;
      if (aJSNode.id && aJSNode.id != -1) {
        // Harvest bookmark-specific properties
        let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(aJSNode.id);
        if (keyword)
          aJSNode.keyword = keyword;
      }

      let tags = aIsUICommand ? aPlacesNode.tags : null;
      if (tags)
        aJSNode.tags = tags;

      // Last character-set
      let uri = NetUtil.newURI(aPlacesNode.uri);
      let lastCharset = yield PlacesUtils.getCharsetForURI(uri)
      if (lastCharset)
        aJSNode.charset = lastCharset;
    });
  },

  _addSeparatorProperties: function BN__addSeparatorProperties(
    aPlacesNode, aJSNode) {
    aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
  },

  _addContainerProperties: function BN__addContainerProperties(
    aPlacesNode, aJSNode, aIsUICommand, aResolveShortcuts) {
    let concreteId = PlacesUtils.getConcreteItemId(aPlacesNode);
    if (concreteId != -1) {
      // This is a bookmark or a tag container.
      if (PlacesUtils.nodeIsQuery(aPlacesNode) ||
          (concreteId != aPlacesNode.itemId && !aResolveShortcuts)) {
        aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE;
        aJSNode.uri = aPlacesNode.uri;
        // Folder shortcut
        if (aIsUICommand)
          aJSNode.concreteId = concreteId;
      } else {
        // Bookmark folder or a shortcut we should convert to folder.
        aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;

        // Mark root folders.
        if (aJSNode.id == PlacesUtils.placesRootId)
          aJSNode.root = "placesRoot";
        else if (aJSNode.id == PlacesUtils.bookmarksMenuFolderId)
          aJSNode.root = "bookmarksMenuFolder";
        else if (aJSNode.id == PlacesUtils.tagsFolderId)
          aJSNode.root = "tagsFolder";
        else if (aJSNode.id == PlacesUtils.unfiledBookmarksFolderId)
          aJSNode.root = "unfiledBookmarksFolder";
        else if (aJSNode.id == PlacesUtils.toolbarFolderId)
          aJSNode.root = "toolbarFolder";
      }
    } else {
      // This is a grouped container query, generated on the fly.
      aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE;
      aJSNode.uri = aPlacesNode.uri;
    }
  },

  _appendConvertedComplexNode: function BN__appendConvertedComplexNode(
    aNode, aSourceNode, aArray, aIsUICommand, aResolveShortcuts,
    aExcludeItems) {
    return Task.spawn(function() {
      let repr = {};
      let nodeCount = 0;

      for (let [name, value] in Iterator(aNode))
        repr[name] = value;

      // Write child nodes
      let children = repr.children = [];
      if (!aNode.livemark) {
        PlacesUtils.asContainer(aSourceNode);
        let wasOpen = aSourceNode.containerOpen;
        if (!wasOpen)
          aSourceNode.containerOpen = true;
        let cc = aSourceNode.childCount;
        for (let i = 0; i < cc; ++i) {
          let childNode = aSourceNode.getChild(i);
          if (aExcludeItems && aExcludeItems.indexOf(childNode.itemId) != -1)
            continue;
          let result = yield this._appendConvertedNode(aSourceNode.getChild(i), i, children,
                                                       aIsUICommand, aResolveShortcuts,
                                                       aExcludeItems);
          nodeCount += result.nodeCount;
        }
        if (!wasOpen)
          aSourceNode.containerOpen = false;
      }

      aArray.push(repr);
      throw new Task.Result(nodeCount);
    }.bind(this));
  }
}