home *** CD-ROM | disk | FTP | other *** search
/ PC go! 2008 October / PCgo 2008-10 (DVD).iso / interface / contents / vollversionen_6617 / 21733 / files / xulrunner / modules / utils.js < prev    next >
Encoding:
JavaScript  |  2008-08-20  |  57.5 KB  |  1,631 lines

  1. /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
  2. /* ***** BEGIN LICENSE BLOCK *****
  3.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  4.  *
  5.  * The contents of this file are subject to the Mozilla Public License Version
  6.  * 1.1 (the "License"); you may not use this file except in compliance with
  7.  * the License. You may obtain a copy of the License at
  8.  * http://www.mozilla.org/MPL/
  9.  *
  10.  * Software distributed under the License is distributed on an "AS IS" basis,
  11.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  12.  * for the specific language governing rights and limitations under the
  13.  * License.
  14.  *
  15.  * The Original Code is the Places Command Controller.
  16.  *
  17.  * The Initial Developer of the Original Code is Google Inc.
  18.  * Portions created by the Initial Developer are Copyright (C) 2005
  19.  * the Initial Developer. All Rights Reserved.
  20.  *
  21.  * Contributor(s):
  22.  *   Ben Goodger <beng@google.com>
  23.  *   Myk Melez <myk@mozilla.org>
  24.  *   Asaf Romano <mano@mozilla.com>
  25.  *   Sungjoon Steve Won <stevewon@gmail.com>
  26.  *   Dietrich Ayala <dietrich@mozilla.com>
  27.  *
  28.  * Alternatively, the contents of this file may be used under the terms of
  29.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  30.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  31.  * in which case the provisions of the GPL or the LGPL are applicable instead
  32.  * of those above. If you wish to allow use of your version of this file only
  33.  * under the terms of either the GPL or the LGPL, and not to allow others to
  34.  * use your version of this file under the terms of the MPL, indicate your
  35.  * decision by deleting the provisions above and replace them with the notice
  36.  * and other provisions required by the GPL or the LGPL. If you do not delete
  37.  * the provisions above, a recipient may use your version of this file under
  38.  * the terms of any one of the MPL, the GPL or the LGPL.
  39.  *
  40.  * ***** END LICENSE BLOCK ***** */
  41.  
  42. function LOG(str) {
  43.   dump("*** " + str + "\n");
  44. }
  45.  
  46. var EXPORTED_SYMBOLS = ["PlacesUtils"];
  47.  
  48. var Ci = Components.interfaces;
  49. var Cc = Components.classes;
  50. var Cr = Components.results;
  51.  
  52. const POST_DATA_ANNO = "bookmarkProperties/POSTData";
  53. const READ_ONLY_ANNO = "placesInternal/READ_ONLY";
  54. const LMANNO_FEEDURI = "livemark/feedURI";
  55. const LMANNO_SITEURI = "livemark/siteURI";
  56.  
  57. //@line 62 "e:\builds\tinderbox\XR-Trunk\WINNT_5.2_Depend\mozilla\toolkit\components\places\src\utils.js"
  58. // On other platforms, the transferable system converts "\r\n" to "\n".
  59. const NEWLINE = "\r\n";
  60. //@line 65 "e:\builds\tinderbox\XR-Trunk\WINNT_5.2_Depend\mozilla\toolkit\components\places\src\utils.js"
  61.  
  62. function QI_node(aNode, aIID) {
  63.   var result = null;
  64.   try {
  65.     result = aNode.QueryInterface(aIID);
  66.   }
  67.   catch (e) {
  68.   }
  69.   return result;
  70. }
  71. function asVisit(aNode)    { return QI_node(aNode, Ci.nsINavHistoryVisitResultNode);    }
  72. function asFullVisit(aNode){ return QI_node(aNode, Ci.nsINavHistoryFullVisitResultNode);}
  73. function asContainer(aNode){ return QI_node(aNode, Ci.nsINavHistoryContainerResultNode);}
  74. function asQuery(aNode)    { return QI_node(aNode, Ci.nsINavHistoryQueryResultNode);    }
  75.  
  76. var PlacesUtils = {
  77.   // Place entries that are containers, e.g. bookmark folders or queries.
  78.   TYPE_X_MOZ_PLACE_CONTAINER: "text/x-moz-place-container",
  79.   // Place entries that are bookmark separators.
  80.   TYPE_X_MOZ_PLACE_SEPARATOR: "text/x-moz-place-separator",
  81.   // Place entries that are not containers or separators
  82.   TYPE_X_MOZ_PLACE: "text/x-moz-place",
  83.   // Place entries in shortcut url format (url\ntitle)
  84.   TYPE_X_MOZ_URL: "text/x-moz-url",
  85.   // Place entries formatted as HTML anchors
  86.   TYPE_HTML: "text/html",
  87.   // Place entries as raw URL text
  88.   TYPE_UNICODE: "text/unicode",
  89.  
  90.   /**
  91.    * The Bookmarks Service.
  92.    */
  93.   get bookmarks() {
  94.     delete this.bookmarks;
  95.     return this.bookmarks = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
  96.                             getService(Ci.nsINavBookmarksService);
  97.   },
  98.  
  99.   /**
  100.    * The Nav History Service.
  101.    */
  102.   get history() {
  103.     delete this.history;
  104.     return this.history = Cc["@mozilla.org/browser/nav-history-service;1"].
  105.                           getService(Ci.nsINavHistoryService);
  106.   },
  107.  
  108.   /**
  109.    * The Live Bookmark Service.
  110.    */
  111.   get livemarks() {
  112.     delete this.livemarks;
  113.     return this.livemarks = Cc["@mozilla.org/browser/livemark-service;2"].
  114.                             getService(Ci.nsILivemarkService);
  115.   },
  116.  
  117.   /**
  118.    * The Annotations Service.
  119.    */
  120.   get annotations() {
  121.     delete this.annotations;
  122.     return this.annotations = Cc["@mozilla.org/browser/annotation-service;1"].
  123.                               getService(Ci.nsIAnnotationService);
  124.   },
  125.  
  126.   /**
  127.    * The Favicons Service
  128.    */
  129.   get favicons() {
  130.     delete this.favicons;
  131.     return this.favicons = Cc["@mozilla.org/browser/favicon-service;1"].
  132.                            getService(Ci.nsIFaviconService);
  133.   },
  134.  
  135.   /**
  136.    * The Places Tagging Service
  137.    */
  138.   get tagging() {
  139.     delete this.tagging;
  140.     return this.tagging = Cc["@mozilla.org/browser/tagging-service;1"].
  141.                           getService(Ci.nsITaggingService);
  142.   },
  143.  
  144.   /**
  145.    * Makes a URI from a spec.
  146.    * @param   aSpec
  147.    *          The string spec of the URI
  148.    * @returns A URI object for the spec.
  149.    */
  150.   _uri: function PU__uri(aSpec) {
  151.     return Cc["@mozilla.org/network/io-service;1"].
  152.            getService(Ci.nsIIOService).
  153.            newURI(aSpec, null, null);
  154.   },
  155.  
  156.   /**
  157.    * String bundle helpers
  158.    */
  159.   get _bundle() {
  160.     const PLACES_STRING_BUNDLE_URI =
  161.         "chrome://places/locale/places.properties";
  162.     delete this._bundle;
  163.     return this._bundle = Cc["@mozilla.org/intl/stringbundle;1"].
  164.                           getService(Ci.nsIStringBundleService).
  165.                           createBundle(PLACES_STRING_BUNDLE_URI);
  166.   },
  167.  
  168.   getFormattedString: function PU_getFormattedString(key, params) {
  169.     return this._bundle.formatStringFromName(key, params, params.length);
  170.   },
  171.  
  172.   getString: function PU_getString(key) {
  173.     return this._bundle.GetStringFromName(key);
  174.   },
  175.  
  176.   /**
  177.    * Determines whether or not a ResultNode is a Bookmark folder.
  178.    * @param   aNode
  179.    *          A result node
  180.    * @returns true if the node is a Bookmark folder, false otherwise
  181.    */
  182.   nodeIsFolder: function PU_nodeIsFolder(aNode) {
  183.     return (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER ||
  184.             aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT);
  185.   },
  186.  
  187.   /**
  188.    * Determines whether or not a ResultNode represents a bookmarked URI.
  189.    * @param   aNode
  190.    *          A result node
  191.    * @returns true if the node represents a bookmarked URI, false otherwise
  192.    */
  193.   nodeIsBookmark: function PU_nodeIsBookmark(aNode) {
  194.     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI &&
  195.            aNode.itemId != -1;
  196.   },
  197.  
  198.   /**
  199.    * Determines whether or not a ResultNode is a Bookmark separator.
  200.    * @param   aNode
  201.    *          A result node
  202.    * @returns true if the node is a Bookmark separator, false otherwise
  203.    */
  204.   nodeIsSeparator: function PU_nodeIsSeparator(aNode) {
  205.  
  206.     return (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR);
  207.   },
  208.  
  209.   /**
  210.    * Determines whether or not a ResultNode is a visit item.
  211.    * @param   aNode
  212.    *          A result node
  213.    * @returns true if the node is a visit item, false otherwise
  214.    */
  215.   nodeIsVisit: function PU_nodeIsVisit(aNode) {
  216.     const NHRN = Ci.nsINavHistoryResultNode;
  217.     var type = aNode.type;
  218.     return type == NHRN.RESULT_TYPE_VISIT ||
  219.            type == NHRN.RESULT_TYPE_FULL_VISIT;
  220.   },
  221.  
  222.   /**
  223.    * Determines whether or not a ResultNode is a URL item.
  224.    * @param   aNode
  225.    *          A result node
  226.    * @returns true if the node is a URL item, false otherwise
  227.    */
  228.   uriTypes: [Ci.nsINavHistoryResultNode.RESULT_TYPE_URI,
  229.              Ci.nsINavHistoryResultNode.RESULT_TYPE_VISIT,
  230.              Ci.nsINavHistoryResultNode.RESULT_TYPE_FULL_VISIT],
  231.   nodeIsURI: function PU_nodeIsURI(aNode) {
  232.     return this.uriTypes.indexOf(aNode.type) != -1;
  233.   },
  234.  
  235.   /**
  236.    * Determines whether or not a ResultNode is a Query item.
  237.    * @param   aNode
  238.    *          A result node
  239.    * @returns true if the node is a Query item, false otherwise
  240.    */
  241.   nodeIsQuery: function PU_nodeIsQuery(aNode) {
  242.     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY;
  243.   },
  244.  
  245.   /**
  246.    * Determines if a node is read only (children cannot be inserted, sometimes
  247.    * they cannot be removed depending on the circumstance)
  248.    * @param   aNode
  249.    *          A result node
  250.    * @returns true if the node is readonly, false otherwise
  251.    */
  252.   nodeIsReadOnly: function PU_nodeIsReadOnly(aNode) {
  253.     if (this.nodeIsFolder(aNode))
  254.       return this.bookmarks.getFolderReadonly(asQuery(aNode).folderItemId);
  255.     if (this.nodeIsQuery(aNode) &&
  256.         asQuery(aNode).queryOptions.resultType !=
  257.           Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_CONTENTS)
  258.       return aNode.childrenReadOnly;
  259.     return false;
  260.   },
  261.  
  262.   /**
  263.    * Determines whether or not a ResultNode is a host container.
  264.    * @param   aNode
  265.    *          A result node
  266.    * @returns true if the node is a host container, false otherwise
  267.    */
  268.   nodeIsHost: function PU_nodeIsHost(aNode) {
  269.     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
  270.            aNode.parent &&
  271.            asQuery(aNode.parent).queryOptions.resultType ==
  272.              Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY;
  273.   },
  274.  
  275.   /**
  276.    * Determines whether or not a ResultNode is a day container.
  277.    * @param   node
  278.    *          A NavHistoryResultNode
  279.    * @returns true if the node is a day container, false otherwise
  280.    */
  281.   nodeIsDay: function PU_nodeIsDay(aNode) {
  282.     var resultType;
  283.     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
  284.            aNode.parent &&
  285.            ((resultType = asQuery(aNode.parent).queryOptions.resultType) ==
  286.                Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
  287.              resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY);
  288.   },
  289.  
  290.   /**
  291.    * Determines whether or not a result-node is a tag container.
  292.    * @param   aNode
  293.    *          A result-node
  294.    * @returns true if the node is a tag container, false otherwise
  295.    */
  296.   nodeIsTagQuery: function PU_nodeIsTagQuery(aNode) {
  297.     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
  298.            asQuery(aNode).queryOptions.resultType ==
  299.              Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_CONTENTS;
  300.   },
  301.  
  302.   /**
  303.    * Determines whether or not a ResultNode is a container.
  304.    * @param   aNode
  305.    *          A result node
  306.    * @returns true if the node is a container item, false otherwise
  307.    */
  308.   containerTypes: [Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
  309.                    Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT,
  310.                    Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY,
  311.                    Ci.nsINavHistoryResultNode.RESULT_TYPE_DYNAMIC_CONTAINER],
  312.   nodeIsContainer: function PU_nodeIsContainer(aNode) {
  313.     return this.containerTypes.indexOf(aNode.type) != -1;
  314.   },
  315.  
  316.   /**
  317.    * Determines whether or not a ResultNode is an history related container.
  318.    * @param   node
  319.    *          A result node
  320.    * @returns true if the node is an history related container, false otherwise
  321.    */
  322.   nodeIsHistoryContainer: function PU_nodeIsHistoryContainer(aNode) {
  323.     var resultType;
  324.     return this.nodeIsQuery(aNode) &&
  325.            ((resultType = asQuery(aNode).queryOptions.resultType) ==
  326.               Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY ||
  327.             resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
  328.             resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY ||
  329.             this.nodeIsDay(aNode) ||
  330.             this.nodeIsHost(aNode));
  331.   },
  332.  
  333.   /**
  334.    * Determines whether or not a result-node is a dynamic-container item.
  335.    * The dynamic container result node type is for dynamically created
  336.    * containers (e.g. for the file browser service where you get your folders
  337.    * in bookmark menus).
  338.    * @param   aNode
  339.    *          A result node
  340.    * @returns true if the node is a dynamic container item, false otherwise
  341.    */
  342.   nodeIsDynamicContainer: function PU_nodeIsDynamicContainer(aNode) {
  343.     if (aNode.type == NHRN.RESULT_TYPE_DYNAMIC_CONTAINER)
  344.       return true;
  345.     return false;
  346.   },
  347.  
  348.  /**
  349.   * Determines whether a result node is a remote container registered by the
  350.   * livemark service.
  351.   * @param aNode
  352.   *        A result Node
  353.   * @returns true if the node is a livemark container item
  354.   */
  355.   nodeIsLivemarkContainer: function PU_nodeIsLivemarkContainer(aNode) {
  356.     // Use the annotations service directly to avoid instantiating
  357.     // the Livemark service on startup. (bug 398300)
  358.     return this.nodeIsFolder(aNode) &&
  359.            this.annotations.itemHasAnnotation(aNode.itemId, LMANNO_FEEDURI);
  360.   },
  361.  
  362.  /**
  363.   * Determines whether a result node is a live-bookmark item
  364.   * @param aNode
  365.   *        A result node
  366.   * @returns true if the node is a livemark container item
  367.   */
  368.   nodeIsLivemarkItem: function PU_nodeIsLivemarkItem(aNode) {
  369.     return aNode.parent && this.nodeIsLivemarkContainer(aNode.parent);
  370.   },
  371.  
  372.   /**
  373.    * Determines whether or not a node is a readonly folder.
  374.    * @param   aNode
  375.    *          The node to test.
  376.    * @returns true if the node is a readonly folder.
  377.   */
  378.   isReadonlyFolder: function(aNode) {
  379.     return this.nodeIsFolder(aNode) &&
  380.            this.bookmarks.getFolderReadonly(asQuery(aNode).folderItemId);
  381.   },
  382.  
  383.   /**
  384.    * Gets the concrete item-id for the given node. Generally, this is just
  385.    * node.itemId, but for folder-shortcuts that's node.folderItemId.
  386.    */
  387.   getConcreteItemId: function PU_getConcreteItemId(aNode) {
  388.     if (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT)
  389.       return asQuery(aNode).folderItemId;
  390.     else if (PlacesUtils.nodeIsTagQuery(aNode)) {
  391.       // RESULTS_AS_TAG_CONTENTS queries are similar to folder shortcuts
  392.       // so we can still get the concrete itemId for them.
  393.       var queries = aNode.getQueries({});
  394.       var folders = queries[0].getFolders({});
  395.       return folders[0];
  396.     }
  397.     return aNode.itemId;
  398.   },
  399.  
  400.   /**
  401.    * Gets the index of a node within its parent container
  402.    * @param   aNode
  403.    *          The node to look up
  404.    * @returns The index of the node within its parent container, or -1 if the
  405.    *          node was not found or the node specified has no parent.
  406.    */
  407.   getIndexOfNode: function PU_getIndexOfNode(aNode) {
  408.     var parent = aNode.parent;
  409.     if (!parent)
  410.       return -1;
  411.     var wasOpen = parent.containerOpen;
  412.     var result, oldViewer;
  413.     if (!wasOpen) {
  414.       result = parent.parentResult;
  415.       oldViewer = result.viewer;
  416.       result.viewer = null;
  417.       parent.containerOpen = true;
  418.     }
  419.     var cc = parent.childCount;
  420.     for (var i = 0; i < cc && parent.getChild(i) != aNode; ++i);
  421.     if (!wasOpen) {
  422.       parent.containerOpen = false;
  423.       result.viewer = oldViewer;
  424.     }
  425.     return i < cc ? i : -1;
  426.   },
  427.  
  428.   /**
  429.    * String-wraps a result node according to the rules of the specified
  430.    * content type.
  431.    * @param   aNode
  432.    *          The Result node to wrap (serialize)
  433.    * @param   aType
  434.    *          The content type to serialize as
  435.    * @param   [optional] aOverrideURI
  436.    *          Used instead of the node's URI if provided.
  437.    *          This is useful for wrapping a container as TYPE_X_MOZ_URL,
  438.    *          TYPE_HTML or TYPE_UNICODE.
  439.    * @param   aForceCopy
  440.    *          Does a full copy, resolving folder shortcuts.
  441.    * @returns A string serialization of the node
  442.    */
  443.   wrapNode: function PU_wrapNode(aNode, aType, aOverrideURI, aForceCopy) {
  444.     var self = this;
  445.  
  446.     // when wrapping a node, we want all the items, even if the original
  447.     // query options are excluding them.
  448.     // this can happen when copying from the left hand pane of the bookmarks
  449.     // organizer
  450.     function convertNode(cNode) {
  451.       if (self.nodeIsFolder(cNode) && asQuery(cNode).queryOptions.excludeItems) {
  452.         var concreteId = self.getConcreteItemId(cNode);
  453.         return self.getFolderContents(concreteId, false, true).root;
  454.       }
  455.       return cNode;
  456.     }
  457.  
  458.     switch (aType) {
  459.       case this.TYPE_X_MOZ_PLACE:
  460.       case this.TYPE_X_MOZ_PLACE_SEPARATOR:
  461.       case this.TYPE_X_MOZ_PLACE_CONTAINER:
  462.         var writer = {
  463.           value: "",
  464.           write: function PU_wrapNode__write(aStr, aLen) {
  465.             this.value += aStr;
  466.           }
  467.         };
  468.         self.serializeNodeAsJSONToOutputStream(convertNode(aNode), writer, true, aForceCopy);
  469.         return writer.value;
  470.       case this.TYPE_X_MOZ_URL:
  471.         function gatherDataUrl(bNode) {
  472.           if (self.nodeIsLivemarkContainer(bNode)) {
  473.             var siteURI = self.livemarks.getSiteURI(bNode.itemId).spec;
  474.             return siteURI + NEWLINE + bNode.title;
  475.           }
  476.           if (self.nodeIsURI(bNode))
  477.             return (aOverrideURI || bNode.uri) + NEWLINE + bNode.title;
  478.           // ignore containers and separators - items without valid URIs
  479.           return "";
  480.         }
  481.         return gatherDataUrl(convertNode(aNode));
  482.  
  483.       case this.TYPE_HTML:
  484.         function gatherDataHtml(bNode) {
  485.           function htmlEscape(s) {
  486.             s = s.replace(/&/g, "&");
  487.             s = s.replace(/>/g, ">");
  488.             s = s.replace(/</g, "<");
  489.             s = s.replace(/"/g, """);
  490.             s = s.replace(/'/g, "'");
  491.             return s;
  492.           }
  493.           // escape out potential HTML in the title
  494.           var escapedTitle = bNode.title ? htmlEscape(bNode.title) : "";
  495.           if (self.nodeIsLivemarkContainer(bNode)) {
  496.             var siteURI = self.livemarks.getSiteURI(bNode.itemId).spec;
  497.             return "<A HREF=\"" + siteURI + "\">" + escapedTitle + "</A>" + NEWLINE;
  498.           }
  499.           if (self.nodeIsContainer(bNode)) {
  500.             asContainer(bNode);
  501.             var wasOpen = bNode.containerOpen;
  502.             if (!wasOpen)
  503.               bNode.containerOpen = true;
  504.  
  505.             var childString = "<DL><DT>" + escapedTitle + "</DT>" + NEWLINE;
  506.             var cc = bNode.childCount;
  507.             for (var i = 0; i < cc; ++i)
  508.               childString += "<DD>"
  509.                              + NEWLINE
  510.                              + gatherDataHtml(bNode.getChild(i))
  511.                              + "</DD>"
  512.                              + NEWLINE;
  513.             bNode.containerOpen = wasOpen;
  514.             return childString + "</DL>" + NEWLINE;
  515.           }
  516.           if (self.nodeIsURI(bNode))
  517.             return "<A HREF=\"" + bNode.uri + "\">" + escapedTitle + "</A>" + NEWLINE;
  518.           if (self.nodeIsSeparator(bNode))
  519.             return "<HR>" + NEWLINE;
  520.           return "";
  521.         }
  522.         return gatherDataHtml(convertNode(aNode));
  523.     }
  524.     // case this.TYPE_UNICODE:
  525.     function gatherDataText(bNode) {
  526.       if (self.nodeIsLivemarkContainer(bNode))
  527.         return self.livemarks.getSiteURI(bNode.itemId).spec;
  528.       if (self.nodeIsContainer(bNode)) {
  529.         asContainer(bNode);
  530.         var wasOpen = bNode.containerOpen;
  531.         if (!wasOpen)
  532.           bNode.containerOpen = true;
  533.  
  534.         var childString = bNode.title + NEWLINE;
  535.         var cc = bNode.childCount;
  536.         for (var i = 0; i < cc; ++i) {
  537.           var child = bNode.getChild(i);
  538.           var suffix = i < (cc - 1) ? NEWLINE : "";
  539.           childString += gatherDataText(child) + suffix;
  540.         }
  541.         bNode.containerOpen = wasOpen;
  542.         return childString;
  543.       }
  544.       if (self.nodeIsURI(bNode))
  545.         return (aOverrideURI || bNode.uri);
  546.       if (self.nodeIsSeparator(bNode))
  547.         return "--------------------";
  548.       return "";
  549.     }
  550.  
  551.     return gatherDataText(convertNode(aNode));
  552.   },
  553.  
  554.   /**
  555.    * Unwraps data from the Clipboard or the current Drag Session.
  556.    * @param   blob
  557.    *          A blob (string) of data, in some format we potentially know how
  558.    *          to parse.
  559.    * @param   type
  560.    *          The content type of the blob.
  561.    * @returns An array of objects representing each item contained by the source.
  562.    */
  563.   unwrapNodes: function PU_unwrapNodes(blob, type) {
  564.     // We split on "\n"  because the transferable system converts "\r\n" to "\n"
  565.     var nodes = [];
  566.     switch(type) {
  567.       case this.TYPE_X_MOZ_PLACE:
  568.       case this.TYPE_X_MOZ_PLACE_SEPARATOR:
  569.       case this.TYPE_X_MOZ_PLACE_CONTAINER:
  570.         var JSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
  571.         nodes = JSON.decode("[" + blob + "]");
  572.         break;
  573.       case this.TYPE_X_MOZ_URL:
  574.         var parts = blob.split("\n");
  575.         // data in this type has 2 parts per entry, so if there are fewer
  576.         // than 2 parts left, the blob is malformed and we should stop
  577.         // but drag and drop of files from the shell has parts.length = 1
  578.         if (parts.length != 1 && parts.length % 2)
  579.           break;
  580.         for (var i = 0; i < parts.length; i=i+2) {
  581.           var uriString = parts[i];
  582.           var titleString = "";
  583.           if (parts.length > i+1)
  584.             titleString = parts[i+1];
  585.           else {
  586.             // for drag and drop of files, try to use the leafName as title
  587.             try {
  588.               titleString = this._uri(uriString).QueryInterface(Ci.nsIURL)
  589.                               .fileName;
  590.             }
  591.             catch (e) {}
  592.           }
  593.           // note:  this._uri() will throw if uriString is not a valid URI
  594.           if (this._uri(uriString)) {
  595.             nodes.push({ uri: uriString,
  596.                          title: titleString ? titleString : uriString ,
  597.                          type: this.TYPE_X_MOZ_URL });
  598.           }
  599.         }
  600.         break;
  601.       case this.TYPE_UNICODE:
  602.         var parts = blob.split("\n");
  603.         for (var i = 0; i < parts.length; i++) {
  604.           var uriString = parts[i];
  605.           // note: this._uri() will throw if uriString is not a valid URI
  606.           if (uriString != "" && this._uri(uriString))
  607.             nodes.push({ uri: uriString,
  608.                          title: uriString,
  609.                          type: this.TYPE_X_MOZ_URL });
  610.         }
  611.         break;
  612.       default:
  613.         LOG("Cannot unwrap data of type " + type);
  614.         throw Cr.NS_ERROR_INVALID_ARG;
  615.     }
  616.     return nodes;
  617.   },
  618.  
  619.   /**
  620.    * Generates a nsINavHistoryResult for the contents of a folder.
  621.    * @param   folderId
  622.    *          The folder to open
  623.    * @param   [optional] excludeItems
  624.    *          True to hide all items (individual bookmarks). This is used on
  625.    *          the left places pane so you just get a folder hierarchy.
  626.    * @param   [optional] expandQueries
  627.    *          True to make query items expand as new containers. For managing,
  628.    *          you want this to be false, for menus and such, you want this to
  629.    *          be true.
  630.    * @returns A nsINavHistoryResult containing the contents of the
  631.    *          folder. The result.root is guaranteed to be open.
  632.    */
  633.   getFolderContents:
  634.   function PU_getFolderContents(aFolderId, aExcludeItems, aExpandQueries) {
  635.     var query = this.history.getNewQuery();
  636.     query.setFolders([aFolderId], 1);
  637.     var options = this.history.getNewQueryOptions();
  638.     options.excludeItems = aExcludeItems;
  639.     options.expandQueries = aExpandQueries;
  640.  
  641.     var result = this.history.executeQuery(query, options);
  642.     result.root.containerOpen = true;
  643.     return result;
  644.   },
  645.  
  646.   /**
  647.    * Fetch all annotations for a URI, including all properties of each
  648.    * annotation which would be required to recreate it.
  649.    * @param aURI
  650.    *        The URI for which annotations are to be retrieved.
  651.    * @return Array of objects, each containing the following properties:
  652.    *         name, flags, expires, mimeType, type, value
  653.    */
  654.   getAnnotationsForURI: function PU_getAnnotationsForURI(aURI) {
  655.     var annosvc = this.annotations;
  656.     var annos = [], val = null;
  657.     var annoNames = annosvc.getPageAnnotationNames(aURI, {});
  658.     for (var i = 0; i < annoNames.length; i++) {
  659.       var flags = {}, exp = {}, mimeType = {}, storageType = {};
  660.       annosvc.getPageAnnotationInfo(aURI, annoNames[i], flags, exp, mimeType, storageType);
  661.       if (storageType.value == annosvc.TYPE_BINARY) {
  662.         var data = {}, length = {}, mimeType = {};
  663.         annosvc.getPageAnnotationBinary(aURI, annoNames[i], data, length, mimeType);
  664.         val = data.value;
  665.       }
  666.       else
  667.         val = annosvc.getPageAnnotation(aURI, annoNames[i]);
  668.  
  669.       annos.push({name: annoNames[i],
  670.                   flags: flags.value,
  671.                   expires: exp.value,
  672.                   mimeType: mimeType.value,
  673.                   type: storageType.value,
  674.                   value: val});
  675.     }
  676.     return annos;
  677.   },
  678.  
  679.   /**
  680.    * Fetch all annotations for an item, including all properties of each
  681.    * annotation which would be required to recreate it.
  682.    * @param aItemId
  683.    *        The identifier of the itme for which annotations are to be
  684.    *        retrieved.
  685.    * @return Array of objects, each containing the following properties:
  686.    *         name, flags, expires, mimeType, type, value
  687.    */
  688.   getAnnotationsForItem: function PU_getAnnotationsForItem(aItemId) {
  689.     var annosvc = this.annotations;
  690.     var annos = [], val = null;
  691.     var annoNames = annosvc.getItemAnnotationNames(aItemId, {});
  692.     for (var i = 0; i < annoNames.length; i++) {
  693.       var flags = {}, exp = {}, mimeType = {}, storageType = {};
  694.       annosvc.getItemAnnotationInfo(aItemId, annoNames[i], flags, exp, mimeType, storageType);
  695.       if (storageType.value == annosvc.TYPE_BINARY) {
  696.         var data = {}, length = {}, mimeType = {};
  697.         annosvc.geItemAnnotationBinary(aItemId, annoNames[i], data, length, mimeType);
  698.         val = data.value;
  699.       }
  700.       else
  701.         val = annosvc.getItemAnnotation(aItemId, annoNames[i]);
  702.  
  703.       annos.push({name: annoNames[i],
  704.                   flags: flags.value,
  705.                   expires: exp.value,
  706.                   mimeType: mimeType.value,
  707.                   type: storageType.value,
  708.                   value: val});
  709.     }
  710.     return annos;
  711.   },
  712.  
  713.   /**
  714.    * Annotate a URI with a batch of annotations.
  715.    * @param aURI
  716.    *        The URI for which annotations are to be set.
  717.    * @param aAnnotations
  718.    *        Array of objects, each containing the following properties:
  719.    *        name, flags, expires, type, mimeType (only used for binary
  720.    *        annotations) value.
  721.    */
  722.   setAnnotationsForURI: function PU_setAnnotationsForURI(aURI, aAnnos) {
  723.     var annosvc = this.annotations;
  724.     aAnnos.forEach(function(anno) {
  725.       var flags = ("flags" in anno) ? anno.flags : 0;
  726.       var expires = ("expires" in anno) ?
  727.         anno.expires : Ci.nsIAnnotationService.EXPIRE_NEVER;
  728.       if (anno.type == annosvc.TYPE_BINARY) {
  729.         annosvc.setPageAnnotationBinary(aURI, anno.name, anno.value,
  730.                                         anno.value.length, anno.mimeType,
  731.                                         flags, expires);
  732.       }
  733.       else
  734.         annosvc.setPageAnnotation(aURI, anno.name, anno.value, flags, expires);
  735.     });
  736.   },
  737.  
  738.   /**
  739.    * Annotate an item with a batch of annotations.
  740.    * @param aItemId
  741.    *        The identifier of the item for which annotations are to be set
  742.    * @param aAnnotations
  743.    *        Array of objects, each containing the following properties:
  744.    *        name, flags, expires, type, mimeType (only used for binary
  745.    *        annotations) value.
  746.    */
  747.   setAnnotationsForItem: function PU_setAnnotationsForItem(aItemId, aAnnos) {
  748.     var annosvc = this.annotations;
  749.     aAnnos.forEach(function(anno) {
  750.       var flags = ("flags" in anno) ? anno.flags : 0;
  751.       var expires = ("expires" in anno) ?
  752.         anno.expires : Ci.nsIAnnotationService.EXPIRE_NEVER;
  753.       if (anno.type == annosvc.TYPE_BINARY) {
  754.         annosvc.setItemAnnotationBinary(aItemId, anno.name, anno.value,
  755.                                         anno.value.length, anno.mimeType,
  756.                                         flags, expires);
  757.       }
  758.       else {
  759.         annosvc.setItemAnnotation(aItemId, anno.name, anno.value, flags,
  760.                                   expires);
  761.       }
  762.     });
  763.   },
  764.  
  765.   /**
  766.    * Helper for getting a serialized Places query for a particular folder.
  767.    * @param aFolderId The folder id to get a query for.
  768.    * @return string serialized place URI
  769.    */
  770.   getQueryStringForFolder: function PU_getQueryStringForFolder(aFolderId) {
  771.     var options = this.history.getNewQueryOptions();
  772.     var query = this.history.getNewQuery();
  773.     query.setFolders([aFolderId], 1);
  774.     return this.history.queriesToQueryString([query], 1, options);
  775.   },
  776.  
  777.   // identifier getters for special folders
  778.   get placesRootId() {
  779.     delete this.placesRootId;
  780.     return this.placesRootId = this.bookmarks.placesRoot;
  781.   },
  782.  
  783.   get bookmarksMenuFolderId() {
  784.     delete this.bookmarksMenuFolderId;
  785.     return this.bookmarksMenuFolderId = this.bookmarks.bookmarksMenuFolder;
  786.   },
  787.  
  788.   get toolbarFolderId() {
  789.     delete this.toolbarFolderId;
  790.     return this.toolbarFolderId = this.bookmarks.toolbarFolder;
  791.   },
  792.  
  793.   get tagsFolderId() {
  794.     delete this.tagsFolderId;
  795.     return this.tagsFolderId = this.bookmarks.tagsFolder;
  796.   },
  797.  
  798.   get unfiledBookmarksFolderId() {
  799.     delete this.unfiledBookmarksFolderId;
  800.     return this.unfiledBookmarksFolderId = this.bookmarks.unfiledBookmarksFolder;
  801.   },
  802.  
  803.   /**
  804.    * Set the POST data associated with a bookmark, if any.
  805.    * Used by POST keywords.
  806.    *   @param aBookmarkId
  807.    *   @returns string of POST data
  808.    */
  809.   setPostDataForBookmark: function PU_setPostDataForBookmark(aBookmarkId, aPostData) {
  810.     const annos = this.annotations;
  811.     if (aPostData)
  812.       annos.setItemAnnotation(aBookmarkId, POST_DATA_ANNO, aPostData, 
  813.                               0, Ci.nsIAnnotationService.EXPIRE_NEVER);
  814.     else if (annos.itemHasAnnotation(aBookmarkId, POST_DATA_ANNO))
  815.       annos.removeItemAnnotation(aBookmarkId, POST_DATA_ANNO);
  816.   },
  817.  
  818.   /**
  819.    * Get the POST data associated with a bookmark, if any.
  820.    * @param aBookmarkId
  821.    * @returns string of POST data if set for aBookmarkId. null otherwise.
  822.    */
  823.   getPostDataForBookmark: function PU_getPostDataForBookmark(aBookmarkId) {
  824.     const annos = this.annotations;
  825.     if (annos.itemHasAnnotation(aBookmarkId, POST_DATA_ANNO))
  826.       return annos.getItemAnnotation(aBookmarkId, POST_DATA_ANNO);
  827.  
  828.     return null;
  829.   },
  830.  
  831.   /**
  832.    * Get the URI (and any associated POST data) for a given keyword.
  833.    * @param aKeyword string keyword
  834.    * @returns an array containing a string URL and a string of POST data
  835.    */
  836.   getURLAndPostDataForKeyword: function PU_getURLAndPostDataForKeyword(aKeyword) {
  837.     var url = null, postdata = null;
  838.     try {
  839.       var uri = this.bookmarks.getURIForKeyword(aKeyword);
  840.       if (uri) {
  841.         url = uri.spec;
  842.         var bookmarks = this.bookmarks.getBookmarkIdsForURI(uri, {});
  843.         for (let i = 0; i < bookmarks.length; i++) {
  844.           var bookmark = bookmarks[i];
  845.           var kw = this.bookmarks.getKeywordForBookmark(bookmark);
  846.           if (kw == aKeyword) {
  847.             postdata = this.getPostDataForBookmark(bookmark);
  848.             break;
  849.           }
  850.         }
  851.       }
  852.     } catch(ex) {}
  853.     return [url, postdata];
  854.   },
  855.  
  856.   /**
  857.    * Get all bookmarks for a URL, excluding items under tag or livemark
  858.    * containers.
  859.    */
  860.   getBookmarksForURI:
  861.   function PU_getBookmarksForURI(aURI) {
  862.     var bmkIds = this.bookmarks.getBookmarkIdsForURI(aURI, {});
  863.  
  864.     // filter the ids list
  865.     return bmkIds.filter(function(aID) {
  866.       var parent = this.bookmarks.getFolderIdForItem(aID);
  867.       // Livemark child
  868.       if (this.annotations.itemHasAnnotation(parent, LMANNO_FEEDURI))
  869.         return false;
  870.       var grandparent = this.bookmarks.getFolderIdForItem(parent);
  871.       // item under a tag container
  872.       if (grandparent == this.tagsFolderId)
  873.         return false;
  874.       return true;
  875.     }, this);
  876.   },
  877.  
  878.   /**
  879.    * Get the most recently added/modified bookmark for a URL, excluding items
  880.    * under tag or livemark containers. -1 is returned if no item is found.
  881.    */
  882.   getMostRecentBookmarkForURI:
  883.   function PU_getMostRecentBookmarkForURI(aURI) {
  884.     var bmkIds = this.bookmarks.getBookmarkIdsForURI(aURI, {});
  885.     for (var i = 0; i < bmkIds.length; i++) {
  886.       // Find the first folder which isn't a tag container
  887.       var bk = bmkIds[i];
  888.       var parent = this.bookmarks.getFolderIdForItem(bk);
  889.       if (parent == this.unfiledBookmarksFolderId)
  890.         return bk;
  891.  
  892.       var grandparent = this.bookmarks.getFolderIdForItem(parent);
  893.       if (grandparent != this.tagsFolderId &&
  894.           !this.annotations.itemHasAnnotation(parent, LMANNO_FEEDURI))
  895.         return bk;
  896.     }
  897.     return -1;
  898.   },
  899.  
  900.   getMostRecentFolderForFeedURI:
  901.   function PU_getMostRecentFolderForFeedURI(aURI) {
  902.     var feedSpec = aURI.spec
  903.     var annosvc = this.annotations;
  904.     var livemarks = annosvc.getItemsWithAnnotation(LMANNO_FEEDURI, {});
  905.     for (var i = 0; i < livemarks.length; i++) {
  906.       if (annosvc.getItemAnnotation(livemarks[i], LMANNO_FEEDURI) == feedSpec)
  907.         return livemarks[i];
  908.     }
  909.     return -1;
  910.   },
  911.  
  912.   // Returns true if a container has uris in its first level
  913.   // Has better performances than checking getURLsForContainerNode(node).length
  914.   hasChildURIs: function PU_hasChildURIs(aNode) {
  915.     if (!this.nodeIsContainer(aNode))
  916.       return false;
  917.  
  918.     // in the Library left pane we use excludeItems
  919.     if (this.nodeIsFolder(aNode) && asQuery(aNode).queryOptions.excludeItems) {
  920.       var itemId = PlacesUtils.getConcreteItemId(aNode);
  921.       var contents = this.getFolderContents(itemId, false, false).root;
  922.       for (var i = 0; i < contents.childCount; ++i) {
  923.         var child = contents.getChild(i);
  924.         if (this.nodeIsURI(child))
  925.           return true;
  926.       }
  927.       return false;
  928.     }
  929.  
  930.     var wasOpen = aNode.containerOpen;
  931.     if (!wasOpen)
  932.       aNode.containerOpen = true;
  933.     var found = false;
  934.     for (var i = 0; i < aNode.childCount && !found; i++) {
  935.       var child = aNode.getChild(i);
  936.       if (this.nodeIsURI(child))
  937.         found = true;
  938.     }
  939.     if (!wasOpen)
  940.       aNode.containerOpen = false;
  941.     return found;
  942.   },
  943.  
  944.   getURLsForContainerNode: function PU_getURLsForContainerNode(aNode) {
  945.     let urls = [];
  946.     if (this.nodeIsFolder(aNode) && asQuery(aNode).queryOptions.excludeItems) {
  947.       // grab manually
  948.       var itemId = this.getConcreteItemId(aNode);
  949.       let contents = this.getFolderContents(itemId, false, false).root;
  950.       for (let i = 0; i < contents.childCount; ++i) {
  951.         let child = contents.getChild(i);
  952.         if (this.nodeIsURI(child))
  953.           urls.push({uri: child.uri, isBookmark: this.nodeIsBookmark(child)});
  954.       }
  955.     }
  956.     else {
  957.       let result, oldViewer, wasOpen;
  958.       try {
  959.         let wasOpen = aNode.containerOpen;
  960.         result = aNode.parentResult;
  961.         oldViewer = result.viewer;
  962.         if (!wasOpen) {
  963.           result.viewer = null;
  964.           aNode.containerOpen = true;
  965.         }
  966.         for (let i = 0; i < aNode.childCount; ++i) {
  967.           // Include visible url nodes only
  968.           let child = aNode.getChild(i);
  969.           if (this.nodeIsURI(child)) {
  970.             // If the node contents is visible, add the uri only if its node is
  971.             // visible. Otherwise follow viewer's collapseDuplicates property,
  972.             // default to true
  973.             if ((wasOpen && oldViewer && child.viewIndex != -1) ||
  974.                 (oldViewer && !oldViewer.collapseDuplicates) ||
  975.                 urls.indexOf(child.uri) == -1) {
  976.               urls.push({ uri: child.uri,
  977.                           isBookmark: this.nodeIsBookmark(child) });
  978.             }
  979.           }
  980.         }
  981.         if (!wasOpen)
  982.           aNode.containerOpen = false;
  983.       }
  984.       finally {
  985.         if (!wasOpen)
  986.           result.viewer = oldViewer;
  987.       }
  988.     }
  989.  
  990.     return urls;
  991.   },
  992.  
  993.   /**
  994.    * Restores bookmarks/tags from a JSON file.
  995.    * WARNING: This method *removes* any bookmarks in the collection before
  996.    * restoring from the file.
  997.    *
  998.    * @param aFile
  999.    *        nsIFile of bookmarks in JSON format to be restored.
  1000.    * @param aExcludeItems
  1001.    *        Array of root item ids (ie: children of the places root)
  1002.    *        to not delete when restoring.
  1003.    */
  1004.   restoreBookmarksFromJSONFile:
  1005.   function PU_restoreBookmarksFromJSONFile(aFile, aExcludeItems) {
  1006.     // open file stream
  1007.     var stream = Cc["@mozilla.org/network/file-input-stream;1"].
  1008.                  createInstance(Ci.nsIFileInputStream);
  1009.     stream.init(aFile, 0x01, 0, 0);
  1010.     var converted = Cc["@mozilla.org/intl/converter-input-stream;1"].
  1011.                     createInstance(Ci.nsIConverterInputStream);
  1012.     converted.init(stream, "UTF-8", 1024,
  1013.                    Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
  1014.  
  1015.     // read in contents
  1016.     var str = {};
  1017.     var jsonStr = "";
  1018.     while (converted.readString(4096, str) != 0)
  1019.       jsonStr += str.value;
  1020.     converted.close();
  1021.  
  1022.     if (jsonStr.length == 0)
  1023.       return; // empty file
  1024.  
  1025.     this.restoreBookmarksFromJSONString(jsonStr, true, aExcludeItems);
  1026.   },
  1027.  
  1028.   /**
  1029.    * Import bookmarks from a JSON string.
  1030.    * 
  1031.    * @param aString
  1032.    *        JSON string of serialized bookmark data.
  1033.    * @param aReplace
  1034.    *        Boolean if true, replace existing bookmarks, else merge.
  1035.    * @param aExcludeItems
  1036.    *        Array of root item ids (ie: children of the places root)
  1037.    *        to not delete when restoring.
  1038.    */
  1039.   restoreBookmarksFromJSONString:
  1040.   function PU_restoreBookmarksFromJSONString(aString, aReplace, aExcludeItems) {
  1041.     // convert string to JSON
  1042.     var nodes = this.unwrapNodes(aString, this.TYPE_X_MOZ_PLACE_CONTAINER);
  1043.  
  1044.     if (nodes.length == 0 || !nodes[0].children ||
  1045.         nodes[0].children.length == 0)
  1046.       return; // nothing to restore
  1047.  
  1048.     // ensure tag folder gets processed last
  1049.     nodes[0].children.sort(function sortRoots(aNode, bNode) {
  1050.       return (aNode.root && aNode.root == "tagsFolder") ? 1 :
  1051.               (bNode.root && bNode.root == "tagsFolder") ? -1 : 0;
  1052.     });
  1053.  
  1054.     var batch = {
  1055.       _utils: this,
  1056.       nodes: nodes[0].children,
  1057.       runBatched: function restore_runBatched() {
  1058.         if (aReplace) {
  1059.           var excludeItems = aExcludeItems || [];
  1060.           // delete existing children of the root node, excepting:
  1061.           // 1. special folders: delete the child nodes 
  1062.           // 2. tags folder: untag via the tagging api
  1063.           var query = this._utils.history.getNewQuery();
  1064.           query.setFolders([this._utils.placesRootId], 1);
  1065.           var options = this._utils.history.getNewQueryOptions();
  1066.           options.expandQueries = false;
  1067.           var root = this._utils.history.executeQuery(query, options).root;
  1068.           root.containerOpen = true;
  1069.           var childIds = [];
  1070.           for (var i = 0; i < root.childCount; i++) {
  1071.             var childId = root.getChild(i).itemId;
  1072.             if (excludeItems.indexOf(childId) == -1)
  1073.               childIds.push(childId);
  1074.           }
  1075.           root.containerOpen = false;
  1076.  
  1077.           for (var i = 0; i < childIds.length; i++) {
  1078.             var rootItemId = childIds[i];
  1079.             if (rootItemId == this._utils.tagsFolderId) {
  1080.               // remove tags via the tagging service
  1081.               var tags = this._utils.tagging.allTags;
  1082.               var uris = [];
  1083.               for (let i in tags) {
  1084.                 var tagURIs = this._utils.tagging.getURIsForTag(tags[i]);
  1085.                 for (let j in tagURIs)
  1086.                   this._utils.tagging.untagURI(tagURIs[j], [tags[i]]);
  1087.               }
  1088.             }
  1089.             else if ([this._utils.toolbarFolderId,
  1090.                       this._utils.unfiledBookmarksFolderId,
  1091.                       this._utils.bookmarksMenuFolderId].indexOf(rootItemId) != -1)
  1092.               this._utils.bookmarks.removeFolderChildren(rootItemId);
  1093.             else
  1094.               this._utils.bookmarks.removeItem(rootItemId);
  1095.           }
  1096.         }
  1097.  
  1098.         var searchIds = [];
  1099.         var folderIdMap = [];
  1100.  
  1101.         this.nodes.forEach(function(node) {
  1102.           if (!node.children || node.children.length == 0)
  1103.             return; // nothing to restore for this root
  1104.  
  1105.           if (node.root) {
  1106.             var container = this.placesRootId; // default to places root
  1107.             switch (node.root) {
  1108.               case "bookmarksMenuFolder":
  1109.                 container = this.bookmarksMenuFolderId;
  1110.                 break;
  1111.               case "tagsFolder":
  1112.                 container = this.tagsFolderId;
  1113.                 break;
  1114.               case "unfiledBookmarksFolder":
  1115.                 container = this.unfiledBookmarksFolderId;
  1116.                 break;
  1117.               case "toolbarFolder":
  1118.                 container = this.toolbarFolderId;
  1119.                 break;
  1120.             }
  1121.  
  1122.             // insert the data into the db
  1123.             node.children.forEach(function(child) {
  1124.               var index = child.index;
  1125.               var [folders, searches] = this.importJSONNode(child, container, index);
  1126.               folderIdMap = folderIdMap.concat(folders);
  1127.               searchIds = searchIds.concat(searches);
  1128.             }, this);
  1129.           }
  1130.           else
  1131.             this.importJSONNode(node, this.placesRootId, node.index);
  1132.  
  1133.         }, this._utils);
  1134.  
  1135.         // fixup imported place: uris that contain folders
  1136.         searchIds.forEach(function(aId) {
  1137.           var oldURI = this.bookmarks.getBookmarkURI(aId);
  1138.           var uri = this._fixupQuery(this.bookmarks.getBookmarkURI(aId),
  1139.                                      folderIdMap);
  1140.           if (!uri.equals(oldURI))
  1141.             this.bookmarks.changeBookmarkURI(aId, uri);
  1142.         }, this._utils);
  1143.       }
  1144.     };
  1145.  
  1146.     this.bookmarks.runInBatchMode(batch, null);
  1147.   },
  1148.  
  1149.   /**
  1150.    * Takes a JSON-serialized node and inserts it into the db.
  1151.    *
  1152.    * @param   aData
  1153.    *          The unwrapped data blob of dropped or pasted data.
  1154.    * @param   aContainer
  1155.    *          The container the data was dropped or pasted into
  1156.    * @param   aIndex
  1157.    *          The index within the container the item was dropped or pasted at
  1158.    * @returns an array containing of maps of old folder ids to new folder ids,
  1159.    *          and an array of saved search ids that need to be fixed up.
  1160.    *          eg: [[[oldFolder1, newFolder1]], [search1]]
  1161.    */
  1162.   importJSONNode: function PU_importJSONNode(aData, aContainer, aIndex) {
  1163.     var folderIdMap = [];
  1164.     var searchIds = [];
  1165.     var id = -1;
  1166.     switch (aData.type) {
  1167.       case this.TYPE_X_MOZ_PLACE_CONTAINER:
  1168.         if (aContainer == PlacesUtils.bookmarks.tagsFolder) {
  1169.           if (aData.children) {
  1170.             aData.children.forEach(function(aChild) {
  1171.               this.tagging.tagURI(this._uri(aChild.uri), [aData.title]);
  1172.             }, this);
  1173.             return [folderIdMap, searchIds];
  1174.           }
  1175.         }
  1176.         else if (aData.livemark && aData.annos) {
  1177.           // node is a livemark
  1178.           var feedURI = null;
  1179.           var siteURI = null;
  1180.           aData.annos = aData.annos.filter(function(aAnno) {
  1181.             if (aAnno.name == LMANNO_FEEDURI) {
  1182.               feedURI = this._uri(aAnno.value);
  1183.               return false;
  1184.             }
  1185.             else if (aAnno.name == LMANNO_SITEURI) {
  1186.               siteURI = this._uri(aAnno.value);
  1187.               return false;
  1188.             }
  1189.             return true;
  1190.           }, this);
  1191.  
  1192.           if (feedURI)
  1193.             id = this.livemarks.createLivemark(aContainer, aData.title, siteURI, feedURI, aIndex);
  1194.         }
  1195.         else {
  1196.           id = this.bookmarks.createFolder(aContainer, aData.title, aIndex);
  1197.           folderIdMap.push([aData.id, id]);
  1198.           // process children
  1199.           if (aData.children) {
  1200.             aData.children.every(function(aChild, aIndex) {
  1201.               var [folderIds, searches] = this.importJSONNode(aChild, id, aIndex);
  1202.               folderIdMap = folderIdMap.concat(folderIds);
  1203.               searchIds = searchIds.concat(searches);
  1204.               return true;
  1205.             }, this);
  1206.           }
  1207.         }
  1208.         break;
  1209.       case this.TYPE_X_MOZ_PLACE:
  1210.         id = this.bookmarks.insertBookmark(aContainer, this._uri(aData.uri), aIndex, aData.title);
  1211.         if (aData.keyword)
  1212.           this.bookmarks.setKeywordForBookmark(id, aData.keyword);
  1213.         if (aData.tags) {
  1214.           var tags = aData.tags.split(", ");
  1215.           if (tags.length)
  1216.             this.tagging.tagURI(this._uri(aData.uri), tags);
  1217.         }
  1218.         if (aData.charset)
  1219.           this.history.setCharsetForURI(this._uri(aData.uri), aData.charset);
  1220.         if (aData.uri.match(/^place:/))
  1221.           searchIds.push(id);
  1222.         break;
  1223.       case this.TYPE_X_MOZ_PLACE_SEPARATOR:
  1224.         id = this.bookmarks.insertSeparator(aContainer, aIndex);
  1225.         break;
  1226.       default:
  1227.     }
  1228.  
  1229.     // set generic properties
  1230.     if (id != -1) {
  1231.       this.bookmarks.setItemDateAdded(id, aData.dateAdded);
  1232.       this.bookmarks.setItemLastModified(id, aData.lastModified);
  1233.       if (aData.annos)
  1234.         this.setAnnotationsForItem(id, aData.annos);
  1235.     }
  1236.  
  1237.     return [folderIdMap, searchIds];
  1238.   },
  1239.  
  1240.   /**
  1241.    * Replaces imported folder ids with their local counterparts in a place: URI.
  1242.    *
  1243.    * @param   aURI
  1244.    *          A place: URI with folder ids.
  1245.    * @param   aFolderIdMap
  1246.    *          An array mapping old folder id to new folder ids.
  1247.    * @returns the fixed up URI if all matched. If some matched, it returns
  1248.    *          the URI with only the matching folders included. If none matched it
  1249.    *          returns the input URI unchanged.
  1250.    */
  1251.   _fixupQuery: function PU__fixupQuery(aQueryURI, aFolderIdMap) {
  1252.     var queries = {};
  1253.     var options = {};
  1254.     this.history.queryStringToQueries(aQueryURI.spec, queries, {}, options);
  1255.  
  1256.     var fixedQueries = [];
  1257.     queries.value.forEach(function(aQuery) {
  1258.       var folders = aQuery.getFolders({});
  1259.  
  1260.       var newFolders = [];
  1261.       aFolderIdMap.forEach(function(aMapping) {
  1262.         if (folders.indexOf(aMapping[0]) != -1)
  1263.           newFolders.push(aMapping[1]);
  1264.       });
  1265.  
  1266.       if (newFolders.length)
  1267.         aQuery.setFolders(newFolders, newFolders.length);
  1268.       fixedQueries.push(aQuery);
  1269.     });
  1270.  
  1271.     var stringURI = this.history.queriesToQueryString(fixedQueries,
  1272.                                                       fixedQueries.length,
  1273.                                                       options.value);
  1274.     return this._uri(stringURI);
  1275.   },
  1276.  
  1277.   /**
  1278.    * Serializes the given node (and all it's descendents) as JSON
  1279.    * and writes the serialization to the given output stream.
  1280.    * 
  1281.    * @param   aNode
  1282.    *          An nsINavHistoryResultNode
  1283.    * @param   aStream
  1284.    *          An nsIOutputStream. NOTE: it only uses the write(str, len)
  1285.    *          method of nsIOutputStream. The caller is responsible for
  1286.    *          closing the stream.
  1287.    * @param   aIsUICommand
  1288.    *          Boolean - If true, modifies serialization so that each node self-contained.
  1289.    *          For Example, tags are serialized inline with each bookmark.
  1290.    * @param   aResolveShortcuts
  1291.    *          Converts folder shortcuts into actual folders. 
  1292.    * @param   aExcludeItems
  1293.    *          An array of item ids that should not be written to the backup.
  1294.    */
  1295.   serializeNodeAsJSONToOutputStream:
  1296.   function PU_serializeNodeAsJSONToOutputStream(aNode, aStream, aIsUICommand,
  1297.                                                 aResolveShortcuts,
  1298.                                                 aExcludeItems) {
  1299.     var self = this;
  1300.     
  1301.     function addGenericProperties(aPlacesNode, aJSNode) {
  1302.       aJSNode.title = aPlacesNode.title;
  1303.       var id = aPlacesNode.itemId;
  1304.       if (id != -1) {
  1305.         aJSNode.id = id;
  1306.  
  1307.         var parent = aPlacesNode.parent;
  1308.         if (parent)
  1309.           aJSNode.parent = parent.itemId;
  1310.         var dateAdded = aPlacesNode.dateAdded;
  1311.         if (dateAdded)
  1312.           aJSNode.dateAdded = dateAdded;
  1313.         var lastModified = aPlacesNode.lastModified;
  1314.         if (lastModified)
  1315.           aJSNode.lastModified = lastModified;
  1316.  
  1317.         // XXX need a hasAnnos api
  1318.         var annos = [];
  1319.         try {
  1320.           annos = self.getAnnotationsForItem(id).filter(function(anno) {
  1321.             // XXX should whitelist this instead, w/ a pref for
  1322.             // backup/restore of non-whitelisted annos
  1323.             // XXX causes JSON encoding errors, so utf-8 encode
  1324.             //anno.value = unescape(encodeURIComponent(anno.value));
  1325.             if (anno.name == LMANNO_FEEDURI)
  1326.               aJSNode.livemark = 1;
  1327.             if (anno.name == READ_ONLY_ANNO && aResolveShortcuts) {
  1328.               // When copying a read-only node, remove the read-only annotation.
  1329.               return false;
  1330.             }
  1331.             return anno.name != "placesInternal/GUID";
  1332.           });
  1333.         } catch(ex) {
  1334.           LOG(ex);
  1335.         }
  1336.         if (annos.length != 0)
  1337.           aJSNode.annos = annos;
  1338.       }
  1339.       // XXXdietrich - store annos for non-bookmark items
  1340.     }
  1341.  
  1342.     function addURIProperties(aPlacesNode, aJSNode) {
  1343.       aJSNode.type = self.TYPE_X_MOZ_PLACE;
  1344.       aJSNode.uri = aPlacesNode.uri;
  1345.       if (aJSNode.id && aJSNode.id != -1) {
  1346.         // harvest bookmark-specific properties
  1347.         var keyword = self.bookmarks.getKeywordForBookmark(aJSNode.id);
  1348.         if (keyword)
  1349.           aJSNode.keyword = keyword;
  1350.       }
  1351.  
  1352.       var tags = aIsUICommand ? aPlacesNode.tags : null;
  1353.       if (tags)
  1354.         aJSNode.tags = tags;
  1355.  
  1356.       // last character-set
  1357.       var uri = self._uri(aPlacesNode.uri);
  1358.       var lastCharset = self.history.getCharsetForURI(uri);
  1359.       if (lastCharset)
  1360.         aJSNode.charset = lastCharset;
  1361.     }
  1362.  
  1363.     function addSeparatorProperties(aPlacesNode, aJSNode) {
  1364.       aJSNode.type = self.TYPE_X_MOZ_PLACE_SEPARATOR;
  1365.     }
  1366.  
  1367.     function addContainerProperties(aPlacesNode, aJSNode) {
  1368.       // saved queries
  1369.       var concreteId = PlacesUtils.getConcreteItemId(aPlacesNode);
  1370.       if (aJSNode.id != -1 && (PlacesUtils.nodeIsQuery(aPlacesNode) ||
  1371.           (concreteId != aPlacesNode.itemId && !aResolveShortcuts))) {
  1372.         aJSNode.type = self.TYPE_X_MOZ_PLACE;
  1373.         aJSNode.uri = aPlacesNode.uri;
  1374.         // folder shortcut
  1375.         if (aIsUICommand)
  1376.           aJSNode.concreteId = concreteId;
  1377.         return;
  1378.       }
  1379.       else if (aJSNode.id != -1) { // bookmark folder
  1380.         if (concreteId != aPlacesNode.itemId)
  1381.         aJSNode.type = self.TYPE_X_MOZ_PLACE;
  1382.         aJSNode.type = self.TYPE_X_MOZ_PLACE_CONTAINER;
  1383.         // mark special folders
  1384.         if (aJSNode.id == self.bookmarks.placesRoot)
  1385.           aJSNode.root = "placesRoot";
  1386.         else if (aJSNode.id == self.bookmarks.bookmarksMenuFolder)
  1387.           aJSNode.root = "bookmarksMenuFolder";
  1388.         else if (aJSNode.id == self.bookmarks.tagsFolder)
  1389.           aJSNode.root = "tagsFolder";
  1390.         else if (aJSNode.id == self.bookmarks.unfiledBookmarksFolder)
  1391.           aJSNode.root = "unfiledBookmarksFolder";
  1392.         else if (aJSNode.id == self.bookmarks.toolbarFolder)
  1393.           aJSNode.root = "toolbarFolder";
  1394.       }
  1395.     }
  1396.  
  1397.     function writeScalarNode(aStream, aNode) {
  1398.       // serialize to json
  1399.       var jstr = self.toJSONString(aNode);
  1400.       // write to stream
  1401.       aStream.write(jstr, jstr.length);
  1402.     }
  1403.  
  1404.     function writeComplexNode(aStream, aNode, aSourceNode) {
  1405.       var escJSONStringRegExp = /(["\\])/g;
  1406.       // write prefix
  1407.       var properties = [];
  1408.       for (let [name, value] in Iterator(aNode)) {
  1409.         if (name == "annos")
  1410.           value = self.toJSONString(value);
  1411.         else if (typeof value == "string")
  1412.           value = "\"" + value.replace(escJSONStringRegExp, '\\$1') + "\"";
  1413.         properties.push("\"" + name.replace(escJSONStringRegExp, '\\$1') + "\":" + value);
  1414.       }
  1415.       var jStr = "{" + properties.join(",") + ",\"children\":[";
  1416.       aStream.write(jStr, jStr.length);
  1417.  
  1418.       // write child nodes
  1419.       if (!aNode.livemark) {
  1420.         asContainer(aSourceNode);
  1421.         var wasOpen = aSourceNode.containerOpen;
  1422.         if (!wasOpen)
  1423.           aSourceNode.containerOpen = true;
  1424.         var cc = aSourceNode.childCount;
  1425.         for (var i = 0; i < cc; ++i) {
  1426.           var childNode = aSourceNode.getChild(i);
  1427.           if (aExcludeItems && aExcludeItems.indexOf(childNode.itemId) != -1)
  1428.             continue;
  1429.           if (i != 0)
  1430.             aStream.write(",", 1);
  1431.           serializeNodeToJSONStream(aSourceNode.getChild(i), i);
  1432.         }
  1433.         if (!wasOpen)
  1434.           aSourceNode.containerOpen = false;
  1435.       }
  1436.  
  1437.       // write suffix
  1438.       aStream.write("]}", 2);
  1439.     }
  1440.  
  1441.     function serializeNodeToJSONStream(bNode, aIndex) {
  1442.       var node = {};
  1443.  
  1444.       // set index in order received
  1445.       // XXX handy shortcut, but are there cases where we don't want
  1446.       // to export using the sorting provided by the query?
  1447.       if (aIndex)
  1448.         node.index = aIndex;
  1449.  
  1450.       addGenericProperties(bNode, node);
  1451.  
  1452.       if (self.nodeIsURI(bNode))
  1453.         addURIProperties(bNode, node);
  1454.       else if (self.nodeIsContainer(bNode))
  1455.         addContainerProperties(bNode, node);
  1456.       else if (self.nodeIsSeparator(bNode))
  1457.         addSeparatorProperties(bNode, node);
  1458.  
  1459.       if (!node.feedURI && node.type == self.TYPE_X_MOZ_PLACE_CONTAINER)
  1460.         writeComplexNode(aStream, node, bNode);
  1461.       else
  1462.         writeScalarNode(aStream, node);
  1463.     }
  1464.  
  1465.     // serialize to stream
  1466.     serializeNodeToJSONStream(aNode, null);
  1467.   },
  1468.  
  1469.   // XXX testing serializers
  1470.   toJSONString: function PU_toJSONString(aObj) {
  1471.     var JSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
  1472.     return JSON.encode(aObj);
  1473.   },
  1474.  
  1475.   /**
  1476.    * Serializes bookmarks using JSON, and writes to the supplied file.
  1477.    */
  1478.   backupBookmarksToFile: function PU_backupBookmarksToFile(aFile, aExcludeItems) {
  1479.     if (aFile.exists() && !aFile.isWritable())
  1480.       return; // XXX
  1481.  
  1482.     // init stream
  1483.     var stream = Cc["@mozilla.org/network/file-output-stream;1"].
  1484.                  createInstance(Ci.nsIFileOutputStream);
  1485.     stream.init(aFile, 0x02 | 0x08 | 0x20, 0600, 0);
  1486.  
  1487.     // utf-8 converter stream
  1488.     var converter = Cc["@mozilla.org/intl/converter-output-stream;1"].
  1489.                  createInstance(Ci.nsIConverterOutputStream);
  1490.     converter.init(stream, "UTF-8", 0, 0x0000);
  1491.  
  1492.     // weep over stream interface variance
  1493.     var streamProxy = {
  1494.       converter: converter,
  1495.       write: function(aData, aLen) {
  1496.         this.converter.writeString(aData);
  1497.       }
  1498.     };
  1499.  
  1500.     // query places root
  1501.     var options = this.history.getNewQueryOptions();
  1502.     options.expandQueries = false;
  1503.     var query = this.history.getNewQuery();
  1504.     query.setFolders([this.bookmarks.placesRoot], 1);
  1505.     var result = this.history.executeQuery(query, options);
  1506.     result.root.containerOpen = true;
  1507.     // serialize as JSON, write to stream
  1508.     this.serializeNodeAsJSONToOutputStream(result.root, streamProxy,
  1509.                                            false, false, aExcludeItems);
  1510.     result.root.containerOpen = false;
  1511.  
  1512.     // close converter and stream
  1513.     converter.close();
  1514.     stream.close();
  1515.   },
  1516.  
  1517.   /**
  1518.    * ArchiveBookmarksFile()
  1519.    *
  1520.    * Creates a dated backup once a day in <profile>/bookmarkbackups.
  1521.    * Stores the bookmarks using JSON.
  1522.    *
  1523.    * @param int aNumberOfBackups - the maximum number of backups to keep
  1524.    *
  1525.    * @param bool aForceArchive - forces creating an archive even if one was 
  1526.    *                             already created that day (overwrites)
  1527.    */
  1528.   archiveBookmarksFile:
  1529.   function PU_archiveBookmarksFile(aNumberOfBackups, aForceArchive) {
  1530.     // get/create backups directory
  1531.     var dirService = Cc["@mozilla.org/file/directory_service;1"].
  1532.                      getService(Ci.nsIProperties);
  1533.     var bookmarksBackupDir = dirService.get("ProfD", Ci.nsILocalFile);
  1534.     bookmarksBackupDir.append("bookmarkbackups");
  1535.     if (!bookmarksBackupDir.exists()) {
  1536.       bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0700);
  1537.       if (!bookmarksBackupDir.exists())
  1538.         return; // unable to create directory!
  1539.     }
  1540.  
  1541.     // construct the new leafname
  1542.     // Use YYYY-MM-DD (ISO 8601) as it doesn't contain illegal characters
  1543.     // and makes the alphabetical order of multiple backup files more useful.
  1544.     var date = new Date().toLocaleFormat("%Y-%m-%d");
  1545.     var backupFilename = this.getFormattedString("bookmarksArchiveFilename", [date]);
  1546.  
  1547.     var backupFile = null;
  1548.     if (!aForceArchive) {
  1549.       var backupFileNames = [];
  1550.       var backupFilenamePrefix = backupFilename.substr(0, backupFilename.indexOf("-"));
  1551.       var entries = bookmarksBackupDir.directoryEntries;
  1552.       while (entries.hasMoreElements()) {
  1553.         var entry = entries.getNext().QueryInterface(Ci.nsIFile);
  1554.         var backupName = entry.leafName;
  1555.         if (backupName.substr(0, backupFilenamePrefix.length) == backupFilenamePrefix) {
  1556.           if (backupName == backupFilename)
  1557.             backupFile = entry;
  1558.           backupFileNames.push(backupName);
  1559.         }
  1560.       }
  1561.  
  1562.       var numberOfBackupsToDelete = 0;
  1563.       if (aNumberOfBackups > -1)
  1564.         numberOfBackupsToDelete = backupFileNames.length - aNumberOfBackups;
  1565.  
  1566.       if (numberOfBackupsToDelete > 0) {
  1567.         // If we don't have today's backup, remove one more so that
  1568.         // the total backups after this operation does not exceed the
  1569.         // number specified in the pref.
  1570.         if (!backupFile)
  1571.           numberOfBackupsToDelete++;
  1572.  
  1573.         backupFileNames.sort();
  1574.         while (numberOfBackupsToDelete--) {
  1575.           let backupFile = bookmarksBackupDir.clone();
  1576.           backupFile.append(backupFileNames[0]);
  1577.           backupFile.remove(false);
  1578.           backupFileNames.shift();
  1579.         }
  1580.       }
  1581.  
  1582.       // do nothing if we either have today's backup already
  1583.       // or the user has set the pref to zero.
  1584.       if (backupFile || aNumberOfBackups == 0)
  1585.         return;
  1586.     }
  1587.  
  1588.     backupFile = bookmarksBackupDir.clone();
  1589.     backupFile.append(backupFilename);
  1590.  
  1591.     if (aForceArchive && backupFile.exists())
  1592.         backupFile.remove(false);
  1593.  
  1594.     if (!backupFile.exists())
  1595.       backupFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
  1596.  
  1597.     this.backupBookmarksToFile(backupFile);
  1598.   },
  1599.  
  1600.   /**
  1601.    * Get the most recent backup file.
  1602.    * @returns nsIFile backup file
  1603.    */
  1604.   getMostRecentBackup: function PU_getMostRecentBackup() {
  1605.     var dirService = Cc["@mozilla.org/file/directory_service;1"].
  1606.                      getService(Ci.nsIProperties);
  1607.     var bookmarksBackupDir = dirService.get("ProfD", Ci.nsILocalFile);
  1608.     bookmarksBackupDir.append("bookmarkbackups");
  1609.     if (!bookmarksBackupDir.exists())
  1610.       return null;
  1611.  
  1612.     var backups = [];
  1613.     var entries = bookmarksBackupDir.directoryEntries;
  1614.     while (entries.hasMoreElements()) {
  1615.       var entry = entries.getNext().QueryInterface(Ci.nsIFile);
  1616.       if (!entry.isHidden() && entry.leafName.match(/^bookmarks-.+(html|json)?$/))
  1617.         backups.push(entry.leafName);
  1618.     }
  1619.  
  1620.     if (backups.length ==  0)
  1621.       return null;
  1622.  
  1623.     backups.sort();
  1624.     var filename = backups.pop();
  1625.  
  1626.     var backupFile = bookmarksBackupDir.clone();
  1627.     backupFile.append(filename);
  1628.     return backupFile;
  1629.   }
  1630. };
  1631.