home *** CD-ROM | disk | FTP | other *** search
/ Clickx 65 / Clickx 65.iso / software / internet / xmarks / xmarks-3.1.1.xpi / chrome / content / foxmarks-places.js < prev    next >
Encoding:
JavaScript  |  2009-05-05  |  41.8 KB  |  1,225 lines

  1. /*
  2.  Copyright 2008 Foxmarks Inc.
  3.  
  4.  foxmarks-places.js: implements class BookmarkDatasource, encapsulating
  5.  the Firefox Places datasource.
  6.  
  7.  */
  8.  
  9. var Cc = Components.classes;
  10. var Ci = Components.interfaces;
  11.  
  12. const MAP_NATIVE_TO_NID = /folder=(\d+)/g
  13. const MAP_NID_TO_NATIVE = /folder=nid-([\w\-]+)/g
  14.  
  15. // Comment out this line and uncomment block below to enable
  16. // collection and reporting of timing info.
  17.  
  18. const collectTimingInfo = false;
  19.  
  20. /*
  21.  
  22. const collectTimingInfo = true;
  23.  
  24. function StartTimes(activity) {
  25.     StartTimes.activity = activity;
  26.     StartTimes.start = Date.now();
  27.     ResetTimes();
  28.     LogWrite("Starting " + activity + "...");
  29. }
  30.  
  31. function AddTime(f, t) {
  32.     if (!AddTime.times) {
  33.         AddTime.times = {};
  34.     }
  35.     if (!AddTime.times[f]) {
  36.         AddTime.times[f] = [];
  37.     }
  38.     AddTime.times[f].push(t);
  39. }
  40.  
  41. function ReportTimes() {
  42.     LogWrite("Total time for " + String(StartTimes.activity) + ": " + 
  43.         String(Date.now() -  StartTimes.start));
  44.  
  45.     function min(a) {
  46.         var m;
  47.         forEach(a, function(v) {
  48.             if (!m || v < m) {
  49.                 m = v;
  50.             }
  51.         });
  52.         return m;
  53.     }
  54.  
  55.     function max(a) {
  56.         var m;
  57.         forEach(a, function(v) {
  58.             if (!m || v > m) {
  59.                 m = v;
  60.             }
  61.         });
  62.         return m;
  63.     }
  64.  
  65.     function avg(a) {
  66.         return (tot(a) / a.length).toPrecision(3);
  67.     }
  68.  
  69.     function tot(a) {
  70.         var m = 0;
  71.         forEach(a, function(v) {
  72.             m += v;
  73.         });
  74.         return m;
  75.     }
  76.  
  77.     forEach(AddTime.times, function(v, k) {
  78.             LogWrite("Time: " + k + ": " + String(v.length) + " times, " +
  79.                 String(tot(v)) + "ms total (" + 
  80.                 String(min(v)) + "/" + String(max(v)) + "/" + String(avg(v)) +
  81.                 ")");
  82.     });
  83. }
  84.  
  85. function ResetTimes() {
  86.     AddTime.times = {};
  87. }
  88.  
  89. */
  90.  
  91. function Call(o, f) {
  92.     const functionRE = /function (\w+)\(\)/
  93.     var args = [];
  94.     if (arguments.length > 2) {
  95.         args = Array.prototype.slice.call(arguments, 2);
  96.     }
  97.  
  98.     try {
  99.         var start = Date.now();
  100.         var returnVal =  f.apply(o, args);
  101.         var end = Date.now();
  102.         if (collectTimingInfo)
  103.             AddTime(f.toSource().match(functionRE)[1], end - start);
  104.         return returnVal;
  105.     } catch (e) {
  106.         throw Error("Places error calling " + f + " with args " + 
  107.                 args.toSource() + " Original error: " + e);
  108.     }
  109. }
  110.  
  111. function ParseIconString(s) {
  112.     var exp = /^data:(.*);base64,(.*)$/;
  113.     var match = exp.exec(s);
  114.  
  115.     if (match) {
  116.         try {
  117.             var data = My_atob(match[2]);
  118.         } catch (e) {
  119.             return null;
  120.         }
  121.         return [match[1] /* mime type */, data /* b64-encoded data */];
  122.     } else {
  123.         return null;
  124.     }
  125. }
  126.  
  127. // Node's representation of dates is seconds since 1/1/1970.
  128. // Mozilla's representation of dates is microseconds since 1/1/1970.
  129. // Use these utility functions to convert between the two formats
  130. // while minimizing rounding error.
  131.  
  132. function DatePlacesToNode(v) {
  133.     return Math.round(v / 1000000);
  134. }
  135.  
  136. function DateNodeToPlaces(v) {
  137.     return (v || 0) * 1000000;
  138. }
  139.  
  140.    
  141.  
  142. function BookmarkDatasource() {
  143.     this.bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
  144.         getService(Ci.nsINavBookmarksService);
  145.     this.hsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
  146.         getService(Ci.nsINavHistoryService);
  147.     this.asvc = Cc["@mozilla.org/browser/annotation-service;1"].
  148.         getService(Ci.nsIAnnotationService);
  149.     this.lmsvc = Cc["@mozilla.org/browser/livemark-service;2"].
  150.         getService(Ci.nsILivemarkService);
  151.     this.tsvc = Cc["@mozilla.org/browser/tagging-service;1"].
  152.         getService(Ci.nsITaggingService);
  153.     this.fisvc = Cc["@mozilla.org/browser/favicon-service;1"].
  154.         getService(Ci.nsIFaviconService);
  155.     this.mssvc = Cc["@mozilla.org/microsummary/service;1"].
  156.         getService(Ci.nsIMicrosummaryService);
  157.     this.fusvc = Cc["@mozilla.org/docshell/urifixup;1"].
  158.         getService(Ci.nsIURIFixup);
  159.  
  160.     if (!BookmarkDatasource._mapNidToNative) {
  161.         this._InitNidNativeMaps();
  162.     }
  163.  
  164.     if (!BookmarkDatasource._locationMap) {
  165.         this._InitLocationMap();
  166.     }
  167.  
  168.     // defined in foxmarks-sync.js
  169.     applyCommonBookmarkFunctions(this);
  170. }
  171.  
  172. BookmarkDatasource.STORAGE_ENGINE = "/Places";
  173. BookmarkDatasource.startupTime = Date.now();
  174. BookmarkDatasource.sessionId = BookmarkDatasource.startupTime.toString(36);
  175. BookmarkDatasource.nidIndex = 0;
  176. BookmarkDatasource.MAX_NID_LENGTH = 32;
  177. BookmarkDatasource.LOCATION_MAP_ANNO_NAME = "foxmarks/locationMap";
  178. BookmarkDatasource.MAP_PLACE_TYPE_TO_NTYPE = 
  179.     { 0 : "bookmark", 5: "query", 6: "folder", 7: "separator", 9: "query" };
  180. BookmarkDatasource.MAP_NTYPE_TO_PLACE_TYPE = { 
  181.     "bookmark": [0, 5, 9], 
  182.     "microsummary": [0], 
  183.     "query": [0, 5, 9], 
  184.     "folder": [6], 
  185.     "feed": [6], 
  186.     "separator": [7]};
  187.  
  188. BookmarkDatasource.MAP_ANNO_TO_NODE = {
  189.     "bookmarkProperties/description"    : ["description"],
  190.     "bookmarkProperties/POSTData"       : ["formdata"],
  191.     "bookmarkProperties/loadInSidebar"  : ["sidebar", 
  192.                                             function(x) { return true; }],
  193.     "livemark/feedURI"                  : ["feedurl"],
  194.     "livemark/siteURI"                  : ["url"],
  195.     "microsummary/generatorURI"         : ["generateduri"],
  196.     "bookmarks/contentType"             : ["contenttype"]
  197. };
  198.  
  199. BookmarkDatasource.MAP_NODE_TO_ANNO = {
  200.     "description"   : "bookmarkProperties/description",
  201.     "formdata"      : "bookmarkProperties/POSTData",
  202.     "sidebar"       : "bookmarkProperties/loadInSidebar",
  203.     "feedurl"       : "livemark/feedURI",
  204.     "generateduri"  : "microsummary/generatorURI",
  205.     "contentType"   : "bookmarks/contentType"
  206. };
  207.  
  208. BookmarkDatasource.INTERESTING_PROPERTIES = {
  209.     "title"         : true,
  210.     "keyword"       : true,
  211.     "uri"           : true
  212. };
  213.  
  214. BookmarkDatasource.KNOWN_ATTRS = {
  215.     "ntype"         : true,
  216.     "description"   : true,
  217.     "formdata"      : true,
  218.     "sidebar"       : true,
  219.     "feedurl"       : true,
  220.     "generateduri"  : true,
  221.     "contentType"   : true,
  222.     "name"          : true,
  223.     "created"       : true,
  224.     "modified"      : true,
  225.     "visited"       : true,
  226.     "shortcuturl"   : true,
  227.     "nid"           : true,
  228.     "pnid"          : true,
  229.     "children"      : true,
  230.     "url"           : true,
  231.     "icon"          : true,
  232.     "tags"          : true,
  233.     "tnid"          : true,
  234.     "unid"          : true
  235. };
  236.  
  237. BookmarkDatasource.MapPlacesToNode = function(place, pnid, children) {
  238.     var self = this;
  239.  
  240.     if (!(place instanceof Ci.nsINavHistoryResultNode)) {
  241.         throw Error("Unknown object type " + place);
  242.     }
  243.  
  244.     if (!(place.type in BookmarkDatasource.MAP_PLACE_TYPE_TO_NTYPE)) {
  245.         LogWrite("Warning: Unhandled result type " + place.type);
  246.         return 0;
  247.     }
  248.  
  249.     var node = new Node(this.MapNative(place.itemId));
  250.     node.pnid = pnid;
  251.     node.ntype = BookmarkDatasource.MAP_PLACE_TYPE_TO_NTYPE[place.type];
  252.     if (node.ntype == 'folder' && Call(this.lmsvc, this.lmsvc.isLivemark, 
  253.             place.itemId)) {
  254.         node.ntype = 'feed';
  255.     } else if (node.ntype == 'bookmark' && 
  256.             Call(this.mssvc, this.mssvc.hasMicrosummary, place.itemId)) {
  257.         node.ntype = 'microsummary';
  258.     }
  259.  
  260.     if (place.title && place.title.length) {
  261.         node.name = place.title;
  262.     }
  263.     if (node.ntype == 'bookmark' || node.ntype == 'query' || 
  264.             node.ntype == 'microsummary') {
  265.         node.url = place.uri;
  266.         if (node.ntype == 'query') {
  267.             try {
  268.                 node.url = node.url.replace(MAP_NATIVE_TO_NID, function(x, y) {
  269.                         return "folder=nid-" + self.MapNative(y, true);
  270.                     });
  271.             } catch (e) {
  272.                 LogWrite("Warning: Failed mapping " + node.toSource());
  273.             }
  274.         } 
  275.     }
  276.     if (place.dateAdded) 
  277.         node.created = DatePlacesToNode(place.dateAdded);
  278.     if (place.lastModified)
  279.         node.modified = DatePlacesToNode(place.lastModified);
  280.     if (place.time)
  281.         node.visited = DatePlacesToNode(place.time);
  282.     var keyword = Call(self.bmsvc, self.bmsvc.getKeywordForBookmark, 
  283.         place.itemId);
  284.     if (keyword) {
  285.         node.shortcuturl = keyword;
  286.     }
  287.     if (node.ntype == 'folder' && children) {
  288.         node.children = children;
  289.     }
  290.  
  291.     if (node.ntype == 'bookmark' && node.url) {
  292.         var uri = self.NewURI(node.url);
  293.         var iconUri = null;
  294.         try {
  295.             iconUri = Call(self.fisvc, self.fisvc.getFaviconForPage, uri);
  296.         } catch(e) {}
  297.         if (iconUri) {
  298.             var mimeType = {};
  299.             var iconData = null;
  300.             try {
  301.                 iconData = Call(self.fisvc, self.fisvc.getFaviconData, iconUri, 
  302.                     mimeType, {});
  303.             } catch(e) {}
  304.             if (iconData && iconData.length > 0) {
  305.                 node.icon = "data:" + mimeType.value + ";base64," + 
  306.                     Base64.encode(iconData, true);
  307.             }
  308.         }
  309.     }
  310.  
  311.     forEach(BookmarkDatasource.MAP_ANNO_TO_NODE, function(attr, k) {
  312.         var value = GetAnnotation(place.itemId, k);
  313.         if (value) {
  314.             node[attr[0]] = attr[1] ? attr[1](value) : value;
  315.         }
  316.     } );
  317.  
  318.     if (place.uri) {
  319.         var uri = self.NewURI(place.uri);
  320.         var tags = uri ? Call(this.tsvc, this.tsvc.getTagsForURI, uri, {}) : [];
  321.         if (tags.length) {
  322.             node.tags = tags.slice().filter(function(x) { 
  323.                     return x && x.length; });
  324.             // Clone it so we get toJSONString, etc.
  325.         }
  326.     }
  327.  
  328.     this.pn.AddNode.apply(this.pn.Caller, [node]);
  329.     return 0;
  330.  
  331.     function GetAnnotation(itemId, anno) {
  332.         try {
  333.             return Call(self.asvc, self.asvc.getItemAnnotation, itemId, anno);
  334.         } catch (e) {
  335.             return null;
  336.         }
  337.     }
  338. }
  339.  
  340. BookmarkDatasource.ProvideNodesDone = function(status) {
  341.     if (!status) {
  342.         // Order is significant here; apply in reverse order
  343.         // from that in which these were accepted.
  344.         this._ApplyLocationMap(this.pn.Caller, 
  345.             this.bmsvc.unfiledBookmarksFolder);
  346.         this._ApplyLocationMap(this.pn.Caller,
  347.             this.bmsvc.toolbarFolder);
  348.         this.pn.Caller.Node(NODE_ROOT, true).tnid =
  349.             this.MapNative(this.bmsvc.toolbarFolder);
  350.         this.pn.Caller.Node(NODE_ROOT, true).unid =
  351.             this.MapNative(this.bmsvc.unfiledBookmarksFolder);
  352.     }
  353.     this.pn.Caller.placesSource = true;
  354.     this.pn.Complete.apply(this.pn.Caller, [status]);
  355.     this.pn = null;
  356.     if (collectTimingInfo)
  357.         ReportTimes();
  358.     return;
  359. }
  360.  
  361. // ot is the static object that maintains state for OnTree,
  362. // the tree-walker.
  363.  
  364. var ot = {}
  365.  
  366. BookmarkDatasource.prototype = {
  367.  
  368.     //
  369.     // Nid <-> Native maps
  370.     //
  371.  
  372.     // We use Places' GUID as our nids. These functions
  373.     // provide mapping services between GUID/nid and the
  374.     // native itemid.
  375.  
  376.     _InitNidNativeMaps: function() {
  377.         BookmarkDatasource._mapNidToNative = {};
  378.         BookmarkDatasource._mapNativeToNid = {};
  379.         this.AddToMap(this.bmsvc.bookmarksMenuFolder, NODE_ROOT);
  380.     },
  381.  
  382.     MapNid: function(nid) {
  383.         return BookmarkDatasource._mapNidToNative[nid] || 
  384.             Call(this.bmsvc, this.bmsvc.getItemIdForGUID, nid);
  385.     },
  386.  
  387.     MapNative: function(itemId, silent) {
  388.  
  389.         // Try the map first.
  390.         var nid = BookmarkDatasource._mapNativeToNid[itemId];
  391.         if (nid)
  392.             return nid;
  393.  
  394.         // Try to get a reasonable length GUID.
  395.         try {
  396.             nid = Call(this.bmsvc, this.bmsvc.getItemGUID, itemId);
  397.         } catch(e) {
  398.             if (!silent) {
  399.                 LogWrite("MapNative failed with itemId = " + itemId);
  400.                 LogWrite("Caller is " + this.MapNative.caller);
  401.             }
  402.             throw e;
  403.         }
  404.         if (nid.length < BookmarkDatasource.MAX_NID_LENGTH)
  405.             return nid;
  406.  
  407.         // No? Make our own.
  408.         nid = this.GenerateNid();
  409.         Call(this.bmsvc, this.bmsvc.setItemGUID, itemId, nid);
  410.         return nid;
  411.     },
  412.  
  413.     AddToMap: function(itemId, nid) {
  414.         BookmarkDatasource._mapNidToNative[nid] = itemId;
  415.         BookmarkDatasource._mapNativeToNid[itemId] = nid;
  416.         return nid;
  417.     },
  418.  
  419.     GenerateNid: function() {
  420.         return BookmarkDatasource.sessionId + "-" +
  421.             (BookmarkDatasource.nidIndex++).toString(36);
  422.     },
  423.  
  424.  
  425.     //
  426.     // LocationMaps
  427.     //
  428.  
  429.     /* For the special folders (toolbar and unfiled), we maintain
  430.        an annotation to persist those folders' location in the nodeset.
  431.        The location is represented as a [pnid, bnid] combination.
  432.        The annotation itself is just the toSource() representation of the
  433.        LocationMap dict, which is keyed off the itemId.
  434.      */
  435.  
  436.     _InitLocationMap: function() {
  437.         try {
  438.             BookmarkDatasource._locationMap = 
  439.                 eval(Call(this.asvc, this.asvc.getItemAnnotation, 
  440.                         this.bmsvc.bookmarksMenuFolder,
  441.                         BookmarkDatasource.LOCATION_MAP_ANNO_NAME));
  442.         } catch(e) {
  443.             BookmarkDatasource._locationMap = {};
  444.             BookmarkDatasource._locationMap[this.bmsvc.toolbarFolder] = 
  445.                 [null, null];
  446.             BookmarkDatasource._locationMap[this.bmsvc.unfiledBookmarksFolder] = 
  447.                 [null, null];
  448.         }
  449.     },
  450.  
  451.     _WriteLocationMap: function() {
  452.         var lm = BookmarkDatasource._locationMap.toSource();
  453.         Call(this.asvc, this.asvc.setItemAnnotation, 
  454.             this.bmsvc.bookmarksMenuFolder,
  455.             BookmarkDatasource.LOCATION_MAP_ANNO_NAME, lm, 0, 
  456.             this.asvc.EXPIRES_NEVER);
  457.     },
  458.  
  459.     _ModifyLocationMap: function(itemId, pnid, bnid) {
  460.         var val = [pnid, bnid];
  461.         if (!equals(BookmarkDatasource._locationMap[itemId], val)) {
  462.             LogWrite("Modifying locationmap for " + itemId + " from " +
  463.                     BookmarkDatasource._locationMap[itemId].toSource() + " to " + 
  464.                     val.toSource());
  465.             BookmarkDatasource._locationMap[itemId] = val;
  466.             this._WriteLocationMap();
  467.         }
  468.     },
  469.  
  470.     _ApplyLocationMap: function(ns, itemId) {
  471.         // Move special node into the correct position
  472.         var v = BookmarkDatasource._locationMap[itemId];
  473.         if (!v)
  474.             return;
  475.  
  476.         var nid = this.MapNative(itemId);
  477.         var pnid = v[0];
  478.         var bnid = v[1];
  479.  
  480.         if (!ns.Node(nid, false, true)) {
  481.             return;
  482.         }
  483.  
  484.         if (!pnid || !ns.Node(pnid, false, true)) {
  485.             pnid = NODE_ROOT;
  486.             bnid = null;
  487.         }
  488.  
  489.         if (bnid && (!ns.Node(pnid)["children"] ||
  490.                 ns.Node(pnid).children.indexOf(bnid) < 0)) {
  491.             bnid = null;
  492.         }
  493.  
  494.         ns.InsertInParent(nid, pnid, bnid);
  495.     },
  496.  
  497.     //
  498.     //
  499.     //
  500.  
  501.     BaselineLoaded: function(baseline, callback) {
  502.         // NYI
  503.         callback(0);
  504.         return;
  505.     },
  506.  
  507.     NewURI: function(str) {
  508.         var uri = null;
  509.         try {
  510.             uri = Cc["@mozilla.org/network/io-service;1"].
  511.                 getService(Ci.nsIIOService).
  512.                 newURI(str, "UTF-8", null);
  513.         } catch(e) {}
  514.         if (!uri) {
  515.             LogWrite("Failed to produce uri for " + str);
  516.         }
  517.         return uri;
  518.     },
  519.  
  520.     FixedURI: function(str) {
  521.         var fixed = null;
  522.         try {
  523.             fixed = Call(this.fusvc, this.fusvc.createFixupURI, str, 0);
  524.         } catch (e) {
  525.             fixed = Call(this.fusvc, this.fusvc.createFixupURI, 
  526.                 "about:blank", 0)
  527.         }
  528.         return fixed;
  529.     },
  530.  
  531.     NormalizeUrl: function(str) {
  532.         return this.FixedURI(str).spec;
  533.     },
  534.  
  535.     runBatched: function() {
  536.         this._func.apply(this, this._args);
  537.     },
  538.  
  539.  
  540.     //
  541.     // Functions for writing to the native store.
  542.     //
  543.  
  544.     AcceptNodes: function(ns, callback) {
  545.         if (collectTimingInfo)
  546.             StartTimes("AcceptNodes");
  547.         this._func = this._AcceptNodes;
  548.         this._args = arguments;
  549.         this.bmsvc.runInBatchMode(this, null);
  550.         if (collectTimingInfo)
  551.             ReportTimes();
  552.     },
  553.  
  554.     _AcceptNodes: function(ns, callback) {
  555.         /*
  556.  
  557.          Here's the strategy. Execute a query on the top-level of the
  558.          bookmarks hierarchy. Push the result for that root onto the stack,
  559.          along with the matching root of the nodeset.
  560.  
  561.          Pop the next pair off the stack. Compare direct properties, and make
  562.          adjustments as necessary. If the nodes being compared are folders
  563.          deal with their children as follows:
  564.  
  565.          (1) First, make sure that all of the children in the node exist
  566.              on the Places side. This means creating entities that don't exist
  567.              at all, and moving entities that exist in a different parent.
  568.          (2) Delete any item that exists on the Places side but not at all
  569.              on in the nodeset.
  570.          (3) Iterate over the node's children, setting position indices in
  571.              the Places children as appropriate.
  572.          (4) Iterate over the node children again, pushing the appropriate
  573.              pairs onto the stack.
  574.  
  575.          Dealing with toolbar: we're going to examine the tnid for the
  576.          incoming nodeset and see if it's the same as the nid for the
  577.          Places toolbarFolder. If it is different, we're
  578.          going to change the nid on the toolbarFolder to match the new tnid.
  579.  
  580.          We'll also be looking at the location of the toolbar, and updating
  581.          the location map if necessary to correspond to the toolbar folder's
  582.          current location.
  583.  
  584.          Finally, we process the toolbar folder separately, dealing with it
  585.          only as a root and not as a child (that is, if we see it come
  586.          through as a child in the nodeset, we will ignore it).
  587.  
  588.          */
  589.  
  590.         var items = [];
  591.         var fixupQueries = [];
  592.         var self = this;
  593.         var status = 0;
  594.  
  595.         var optimizeOK =  ns._cloneSource && ns._cloneSource.placesSource;
  596.  
  597.         try {
  598.             var tnid = ns.Node(NODE_ROOT).tnid;
  599.             var unid = ns.Node(NODE_ROOT).unid;
  600.  
  601.             var specialNids = [];
  602.             AcceptNewRoot(this.bmsvc.toolbarFolder, tnid);
  603.             AcceptNewRoot(this.bmsvc.unfiledBookmarksFolder, unid);
  604.  
  605.             this.PushRoot(this.bmsvc.bookmarksMenuFolder, items);
  606.             this.PushRoot(this.bmsvc.toolbarFolder, items);
  607.             this.PushRoot(this.bmsvc.unfiledBookmarksFolder, items);
  608.  
  609.             while (items.length) {
  610.                 var item = items.shift();
  611.                 var place = item[0];
  612.                 var itemId = place.itemId;
  613.                 var nid = item[1];
  614.                 var node = ns.Node(nid, false, true);
  615.                 if (!node) {
  616.                     // Node in question was deleted; sync children
  617.                     // and be done.
  618.                     SynchronizeChildren(true);
  619.                     continue;
  620.                 }
  621.                 if (MAP_NID_TO_NATIVE.test(node.url)) {
  622.                     fixupQueries.push(itemId);
  623.                 }
  624.                 var uri = self.FixedURI(node.url);
  625.  
  626.                 if (!optimizeOK || ns._node[nid]) {
  627.                     SynchronizeDirectProperties();
  628.                     SynchronizeIcons();
  629.                     SynchronizeAnnotations();
  630.                     SynchronizeTags();
  631.                     SynchronizeChildren(false);
  632.                 }
  633.  
  634.                 if (node.ntype == 'folder' &&
  635.                         place instanceof Ci.nsINavHistoryContainerResultNode) {
  636.                     place.containerOpen = true;
  637.                     for (var i = 0; i < place.childCount; ++i) {
  638.                         var child = place.getChild(i);
  639.                         items.push([child, self.MapNative(child.itemId), nid]);
  640.                     }
  641. // XXX: Firefox incorrectly garbage collects if we close.
  642. //                    place.containerOpen = false;
  643.                 }
  644.             }
  645.  
  646.             forEach(fixupQueries, function(itemId) {
  647.                 try {
  648.                     var uri = Call(self.bmsvc, self.bmsvc.getBookmarkURI, 
  649.                         itemId).spec;
  650.                     uri = uri.replace(MAP_NID_TO_NATIVE, function(x, y) {
  651.                         return "folder=" + self.MapNid(y);
  652.                     });
  653.                     Call(self.bmsvc, self.bmsvc.changeBookmarkURI, itemId, 
  654.                         self.NewURI(uri));
  655.                 } catch(e) {
  656.                     LogWrite("Warning: Couldn't map to nid; error is " + e)
  657.                 }
  658.             });
  659.                 
  660.         } catch(e) {
  661.             LogWrite("Exception in AcceptNodes: " + e);
  662.             status = 3;
  663.         }
  664.         callback(status);
  665.         return;
  666.  
  667.         function AcceptNewRoot(itemId, nid) {
  668.             var nidValid = nid && ns.Node(nid, false, true) != null;
  669.             if (!nidValid || self.MapNative(itemId) != nid) {
  670.                 optimizeOK = false;     // Can't optimize write if root changes.
  671.                 var oldId = Call(self.bmsvc, self.bmsvc.getItemIdForGUID, nid);
  672.                 if (oldId >= 0) {
  673.                     // If the folder that is the new root exists as an
  674.                     // ordinary folder in places, make the old folder itself go
  675.                     // away by giving it a new nid. (It will get deleted after 
  676.                     // its children get moved into the new place.)
  677.                     Call(self.bmsvc, self.bmsvc.setItemGUID, oldId, 
  678.                         self.GenerateNid());
  679.                 }
  680.                 Call(self.bmsvc, self.bmsvc.setItemGUID, itemId, 
  681.                     nidValid ? nid : self.GenerateNid());
  682.             }
  683.  
  684.             if (nidValid) {
  685.                 self._ModifyLocationMap(itemId, ns.Node(nid).pnid,
  686.                     NextSibling(nid));
  687.             } else {
  688.                 self._ModifyLocationMap(itemId, null, null);
  689.             }
  690.             specialNids.push(nid);
  691.         }
  692.  
  693.         function NextSibling(nid) {
  694.             var siblings = ns.Node(ns.Node(nid).pnid).children;
  695.             var index = siblings.indexOf(nid) + 1;
  696.             while (specialNids.indexOf(siblings[index]) >= 0 &&
  697.                     index < siblings.length) {
  698.                 index++;
  699.             }
  700.             return siblings[index];
  701.         }
  702.  
  703.         function SynchronizeDirectProperties() {
  704.             if (BookmarkDatasource.MAP_NTYPE_TO_PLACE_TYPE[node.ntype].
  705.                     indexOf(place.type) < 0) {
  706.                 throw Error("Type mismatch for " + ns.NodeName(nid) +
  707.                     "place.type = " + place.type + " node.ntype = " +
  708.                     node.ntype);
  709.             }
  710.  
  711.             if (node.ntype == 'bookmark' || node.ntype == 'query' ||
  712.                     node.ntype == 'microsummary') {
  713.                 if (place.uri != node.url) {
  714.                     Call(self.bmsvc, self.bmsvc.changeBookmarkURI, itemId, uri);
  715.                     // Conversion to URI may alter exact text, so
  716.                     // modify it in incoming node if it changed.
  717.                     if (uri.spec != node.url) {
  718.                         ns.Node(nid, true).url = uri.spec;
  719.                     }
  720.                 }
  721.             } else if (node.ntype == 'feed') {
  722.                 Call(self.asvc, self.asvc.setItemAnnotation, itemId, 
  723.                     "livemark/siteURI", node.url || "", 0, 
  724.                     self.asvc.EXPIRE_NEVER);
  725.             } else if (node.ntype == 'separator') {
  726.                 if (node.name) {
  727.                     delete node["name"];
  728.                 }
  729.             }
  730.  
  731.             if (place.title != node.name) {
  732.                 Call(self.bmsvc, self.bmsvc.setItemTitle, itemId, 
  733.                     node.name || "");
  734.             }
  735.  
  736.             if (DatePlacesToNode(place.dateAdded) != (node.created || 0)) {
  737.                 Call(self.bmsvc, self.bmsvc.setItemDateAdded, itemId, 
  738.                     DateNodeToPlaces(node.created));
  739.             }
  740.  
  741.             if (DatePlacesToNode(place.lastModified) != (node.modified || 0)) {
  742.                 Call(self.bmsvc, self.bmsvc.setItemLastModified, itemId, 
  743.                     DateNodeToPlaces(node.modified));
  744.             }
  745.  
  746.             if (Call(self.bmsvc, self.bmsvc.getKeywordForBookmark, itemId) !=
  747.                     node.shortcuturl) {
  748.                 Call(self.bmsvc, self.bmsvc.setKeywordForBookmark, itemId, 
  749.                     node.shortcuturl);
  750.             }
  751.  
  752.             forEach(node, function(v, k) {
  753.                 if (!BookmarkDatasource.KNOWN_ATTRS[k] && 
  754.                         typeof v != "function") {
  755.                     LogWrite("Warning: deleting unknown attr " + k);
  756.                     delete ns.Node(nid, true)[k];
  757.                 }
  758.             });
  759.         }
  760.  
  761.         function SynchronizeIcons() {
  762.             if (node.url && uri && node.ntype == 'bookmark' || 
  763.                     node.ntype == 'microsummary') {
  764.                 var parsedIcon = node.icon ? 
  765.                     ParseIconString(node.icon) : null;
  766.                 if (parsedIcon) {
  767.                     // To store favicon data:
  768.                     // (1) Compare it against what we've already got.
  769.                     //     If it's unchanged, skip it.
  770.                     // (2) Generate a new uri.
  771.                     // (3) Store the data for that uri.
  772.                     // (4) Set the favicon uri for the node url.
  773.  
  774.                     var iconUri = null;
  775.                     try {
  776.                         iconUri = Call(self.fisvc, self.fisvc.getFaviconForPage,
  777.                             uri);
  778.                     } catch(e) {}
  779.                     if (iconUri) {
  780.                         var mimeType = {};
  781.                         var iconData = Call(self.fisvc, 
  782.                             self.fisvc.getFaviconData, iconUri, mimeType, {});
  783.                     }
  784.  
  785.                     if (!iconUri || mimeType != parsedIcon[0] || iconData != 
  786.                             parsedIcon[1]) {
  787.                         var newIconUri = 
  788.                             self.NewURI("http://icon.xmarks.com/" +
  789.                                 self.GenerateNid());
  790.                         Call(self.fisvc, self.fisvc.setFaviconData, newIconUri, 
  791.                             parsedIcon[1], parsedIcon[1].length, parsedIcon[0],
  792.                             Number.MAX_VALUE);
  793.                         Call(self.fisvc, self.fisvc.setFaviconUrlForPage, uri, 
  794.                             newIconUri);
  795.                     }
  796.                 } else {
  797.                     // Clear icon if it exists.
  798.                     var iconUri = null;
  799.                     try {
  800.                         var iconUri = Call(self.fisvc, 
  801.                             self.fisvc.getFaviconForPage, uri);
  802.                     } catch (e) {}
  803.                     if (iconUri) {
  804.                         Call(self.fisvc, self.fisvc.setFaviconData, iconUri, 
  805.                             null, 0, null, 0);
  806.                     }
  807.                     // Clear incoming icon if it existed and was invalid.
  808.                     if (node.icon) {
  809.                         ns.Node(nid, true).icon = null;
  810.                     }
  811.                 }
  812.             }
  813.         }
  814.  
  815.         function SynchronizeAnnotations() {
  816.             // Annotation stragegy:
  817.             // (1) Get a list of all annotations for this place.
  818.             // (2) Filter that list down to the annos we're interested in.
  819.             // (3) Build a list of annotation-stored attributes set in the node.
  820.             // (4) Sync the two lists: process changes, adds, deletes.
  821.  
  822.             var placeAnnos = Call(self.asvc, self.asvc.getItemAnnotationNames, 
  823.                 itemId, {});
  824.             placeAnnos = placeAnnos.filter(function(x) { 
  825.                 return (x in BookmarkDatasource.MAP_ANNO_TO_NODE);
  826.             });
  827.  
  828.             var nodeAnnos = [];
  829.             forEach(BookmarkDatasource.MAP_NODE_TO_ANNO, function(v, x) { 
  830.                 if (node[x]) {
  831.                     nodeAnnos.push(x);
  832.                 }
  833.             });
  834.  
  835.             // Exceptional case: if it's a feed, the url maps to
  836.             // the livemark/siteURI annotation.
  837.             if (node.ntype == 'feed' && node.url) {
  838.                 nodeAnnos.push('url');
  839.             }
  840.  
  841.             forEach(nodeAnnos, function(attr) {
  842.                 var anno = BookmarkDatasource.MAP_NODE_TO_ANNO[attr];
  843.                 if (node.ntype == 'feed' && attr == 'url')
  844.                     anno = "livemark/siteURI";  // Exception
  845.                 if (placeAnnos.indexOf(anno) >= 0) {
  846.                     placeAnnos.splice(placeAnnos.indexOf(anno), 1);
  847.                     var value = Call(self.asvc, self.asvc.getItemAnnotation, 
  848.                         itemId, anno);
  849.                     if (value == node[attr]) {
  850.                         return;
  851.                     }
  852.                 }
  853.                 Call(self.asvc, self.asvc.setItemAnnotation, itemId, anno, 
  854.                     node[attr], 0, self.asvc.EXPIRE_NEVER);
  855.             });
  856.  
  857.             forEach(placeAnnos, function(anno) {
  858.                 Call(self.asvc, self.asvc.removeItemAnnotation, itemId, anno);
  859.             });
  860.         }
  861.  
  862.         function SynchronizeTags() {
  863.             // Sync tags
  864.             var tags = uri ? 
  865.                     Call(self.tsvc, self.tsvc.getTagsForURI, uri, {}).slice() :
  866.                     [];
  867.             var ntags = node.tags || [];
  868.             if (!equals(tags, ntags)) {
  869.                 // Delete extraneous tags
  870.                 var extra = tags.filter(function(x) { 
  871.                         return x && x.length && ntags.indexOf(x) < 0; } );
  872.                 if (extra.length) {
  873.                     Call(self.tsvc, self.tsvc.untagURI, uri, extra);
  874.                 }
  875.  
  876.                 // Add missing tags
  877.                 var missing = ntags.filter(function(x) {
  878.                         return x.length && tags.indexOf(x) < 0; } );
  879.                 if (missing.length) {
  880.                     Call(self.tsvc, self.tsvc.tagURI, uri, missing);
  881.                 }
  882.             }
  883.         }
  884.  
  885.         function SynchronizeChildren(isDeleted) {
  886.             if (isDeleted) {
  887.                 node = new Node(nid, { ntype: "folder" } );
  888.             }
  889.  
  890.             if (!(node.ntype == 'folder' &&
  891.                     place instanceof Ci.nsINavHistoryContainerResultNode))
  892.                 return;
  893.  
  894.             // Deal with children.
  895.             place.containerOpen = true;
  896.  
  897.             var children = [];
  898.             for (var i = 0; i < place.childCount; ++i) {
  899.                 var child = place.getChild(i);
  900.                 children.push(self.MapNative(child.itemId));
  901.             }
  902.  
  903.             if (!node.children) {
  904.                 node.children = [];
  905.             }
  906.  
  907.             // Filter "special" children.
  908.             var nodeChildren = node.children.filter(function(x) {
  909.                 return x != tnid && x != unid;
  910.             } );
  911.  
  912.             // Do inserts and moves.
  913.             forEach(nodeChildren, function(child) {
  914.                 if (children.indexOf(child) >= 0) {
  915.                     return;
  916.                 }
  917.                 if (Call(self.bmsvc, self.bmsvc.getItemIdForGUID, child) >= 0) {
  918.                     LogWrite("Moving item " + ns.NodeName(child) + " to " + ns.NodeName(self.MapNative(itemId)));
  919.                     Call(self.bmsvc, self.bmsvc.moveItem, self.MapNid(child),
  920.                          itemId, -1);
  921.                     return;
  922.                 }
  923.                 var cnode = ns.Node(child);
  924.                 var newItemId = null;
  925.                 LogWrite("Creating " + cnode.ntype + " " + ns.NodeName(child) + " ...");
  926.                 switch (cnode.ntype) {
  927.                 case 'bookmark': 
  928.                 case 'microsummary':
  929.                 case 'query':
  930.                     newItemId = Call(self.bmsvc, self.bmsvc.insertBookmark, 
  931.                         itemId, self.FixedURI(cnode.url), -1, cnode.name || "");
  932.                     LogWrite("Bookmark created.");
  933.                     break;
  934.                 case 'folder':
  935.                     newItemId = Call(self.bmsvc, self.bmsvc.createFolder, 
  936.                         itemId, cnode.name || "", -1);
  937.                     LogWrite("Folder created.");
  938.                     break;
  939.                 case 'separator':
  940.                     newItemId = Call(self.bmsvc, self.bmsvc.insertSeparator, 
  941.                         itemId, -1);
  942.                     LogWrite("Separator created");
  943.                     // XXX: Separator name?
  944.                     break;
  945.                 case 'feed':
  946.                     newItemId = Call(self.lmsvc, self.lmsvc.createLivemark, 
  947.                         itemId, cnode.name || "", self.FixedURI(cnode.url), 
  948.                         self.FixedURI(cnode.feedurl), -1);
  949.                     LogWrite("Feed created.");
  950.                     break;
  951.                 }
  952.                 if (newItemId) {
  953.                     Call(self.bmsvc, self.bmsvc.setItemGUID, newItemId, child);
  954.                 }
  955.             });
  956.  
  957.             // Do deletes.
  958.             for (var i = 0; i < place.childCount; ++i) {
  959.                 var childItemId = Call(place, place.getChild, i).itemId;
  960.                 if (ns.Node(self.MapNative(childItemId), false, true)) {
  961.                     continue;
  962.                 }
  963.                 try {
  964.                     LogWrite("Removing " + childItemId + " : " + 
  965.                             ns.NodeName(self.MapNative(childItemId)));
  966.                     Call(self.bmsvc, self.bmsvc.removeChildAt, itemId, i--);
  967.                 } catch(e) {
  968.                     LogWrite("Warning: failed trying to remove " +
  969.                         self.MapNative(childItemId));
  970.                     LogWrite("Error was " + e);
  971.                     i++;
  972.                 }
  973.             }
  974.  
  975.             // Do reorders.
  976.             var extraIndex = nodeChildren.length;
  977.             var reorders = [];
  978.             for (var i = 0; i < place.childCount; ++i) {
  979.                 var childItemId = place.getChild(i).itemId;
  980.                 var index = nodeChildren.indexOf(self.MapNative(childItemId));
  981.                 if (index < 0) {
  982.                     index = extraIndex++;
  983.                 }
  984.                 if (Call(self.bmsvc, self.bmsvc.getItemIndex, childItemId) != 
  985.                         index) {
  986.                     reorders.push([childItemId, index]);
  987.                 }
  988.             }
  989.  
  990.             reorders.sort(function(a, b) { return a[1] - b[1]; });
  991.  
  992.             /*
  993.             if (reorders.length) {
  994.                 LogWrite("Executing reoders in folder " + ns.NodeName(node.nid));
  995.                 LogWrite("reorders is " + reorders.toSource());
  996.                 LogWrite("nodeChildren is " + nodeChildren.toSource());
  997.                 for (var i = 0; i < place.childCount; ++i) {
  998.                     var childItemId = place.getChild(i).itemId;
  999.                     LogWrite("child " + i + " : " + childItemId + " is " +
  1000.                             ns.NodeName(self.MapNative(childItemId)));
  1001.                 }
  1002.             }
  1003. */
  1004.  
  1005.             forEach(reorders, function(r) {
  1006.                 LogWrite("Setting index for " + 
  1007.                     ns.NodeName(self.MapNative(r[0])) + " to " + r[1]);
  1008.                 Call(self.bmsvc, self.bmsvc.setItemIndex, r[0], r[1]);
  1009.             } );
  1010.                 
  1011. // XXX: Firefox incorrectly garbage collects if we close.
  1012. //            place.containerOpen = false;
  1013.         }
  1014.     },
  1015.  
  1016.     //
  1017.     // Functions for reading from the native store.
  1018.     //
  1019.  
  1020.     notify: function(timer) {
  1021.         var self = ot.self;
  1022.         self._func = self._OnTree;
  1023.         self._args = null;
  1024.         this.bmsvc.runInBatchMode(this, null);
  1025.     },
  1026.  
  1027.     _OnTree: function() {
  1028.         var self = ot.self;
  1029.         var items = ot.items;
  1030.         var result;
  1031.         var s = Date.now();
  1032.         while (items.length > 0 && Date.now() - s < 100) {
  1033.             var next = items.shift();
  1034.             var item = next[0];
  1035.             var pnid = next[2];
  1036.             var children = null;
  1037.  
  1038.             if (self.IsContainer(item)) {
  1039.                 children = self.PushChildren(item, items, ot.depthFirst);
  1040.             }
  1041.  
  1042.             try {
  1043.                 result = ot.action.apply(ot.Caller, [item, pnid, children]);
  1044.             } catch (e) {
  1045.                 LogWrite("OnTree error: " + e);
  1046.                 result = 3;
  1047.             }
  1048.  
  1049.             if (result)
  1050.                 break;
  1051.         }
  1052.  
  1053.         if (items.length > 0 && !result) {
  1054.             ot.timer.initWithCallback(self, 10,
  1055.                 Ci.nsITimer.TYPE_ONE_SHOT);
  1056.         } else {
  1057.             ot.complete.apply(ot.Caller, [result]);
  1058.         }
  1059.     },
  1060.  
  1061.     OnTree: function(Caller, action, complete) {
  1062.         ot = {}
  1063.         ot.self = this;
  1064.         ot.Caller = Caller;
  1065.         ot.action = action;
  1066.         ot.complete = complete;
  1067.         ot.depthfirst = false;
  1068.         ot.items = []
  1069.  
  1070.         this.PushRoot(this.bmsvc.bookmarksMenuFolder, ot.items);
  1071.         this.PushRoot(this.bmsvc.toolbarFolder, ot.items);
  1072.         this.PushRoot(this.bmsvc.unfiledBookmarksFolder, ot.items);
  1073.  
  1074.         ot.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  1075.         ot.timer.initWithCallback(this, 10, Ci.nsITimer.TYPE_ONE_SHOT);
  1076.  
  1077.         return;
  1078.     },
  1079.  
  1080.     PushRoot: function(itemId, list) {
  1081.         var nid = null;
  1082.         try {
  1083.             nid = this.MapNative(itemId);
  1084.         } catch(e) {
  1085.             LogWrite("Error: PushRoot couldn't find root for " + itemId);
  1086.         }
  1087.  
  1088.         if (!nid) {
  1089.             return false;
  1090.         }
  1091.  
  1092.         var options = Call(this.hsvc, this.hsvc.getNewQueryOptions);
  1093.         var query = Call(this.hsvc, this.hsvc.getNewQuery);
  1094.         query.setFolders([itemId], 1);
  1095.         var result = Call(this.hsvc, this.hsvc.executeQuery, query, options);
  1096.         list.push([result.root, nid, null]);
  1097.         return true;
  1098.     },
  1099.  
  1100.     IsContainer: function(item) {
  1101.         return (BookmarkDatasource.MAP_PLACE_TYPE_TO_NTYPE[item.type] == 
  1102.                 'folder' && !this.lmsvc.isLivemark(item.itemId));
  1103.     },
  1104.  
  1105.     ProvideNodes: function(Caller, AddNode, Complete) {
  1106.         if (collectTimingInfo)
  1107.             StartTimes("ProvideNodes");
  1108.         this.pn = {}
  1109.         this.pn.Caller = Caller;
  1110.         this.pn.AddNode = AddNode;
  1111.         this.pn.Complete = Complete;
  1112.         this.OnTree(this, BookmarkDatasource.MapPlacesToNode, 
  1113.             BookmarkDatasource.ProvideNodesDone);
  1114.         return;
  1115.     },
  1116.  
  1117.     // Enumerate the children of item and push them
  1118.     // onto the provided list, according to depthFirst ordering.
  1119.     // Returns the list of child nids.
  1120.  
  1121.     PushChildren: function(item, list, depthFirst) {
  1122.         if (!(item instanceof Ci.nsINavHistoryContainerResultNode)) {
  1123.             throw Error("Expected a folder but got a non-container");
  1124.         }
  1125.  
  1126.         item.containerOpen = true;
  1127.         var pnid = this.MapNative(item.itemId);
  1128.         var cnids = []
  1129.  
  1130.         for (var i = 0; i < item.childCount; ++i) {
  1131.             child = item.getChild(i);
  1132.             var cnid = this.MapNative(child.itemId)
  1133.             cnids.push(cnid);
  1134.             if (depthFirst) {
  1135.                 list.splice(i, 0, [child, cnid, pnid]);
  1136.             } else {
  1137.                 list.push([child, cnid, pnid]);
  1138.             }
  1139.         }
  1140. // XXX: Firefox incorrectly garbage collects if we close.
  1141. //        item.containerOpen = false;
  1142.         return cnids;
  1143.     },
  1144.  
  1145.     //
  1146.     // Observer functions.
  1147.     //
  1148.  
  1149.     WatchForChanges: function() {
  1150.         var watcher = new BookmarkWatcher();
  1151.         // start observing
  1152.         Call(this.bmsvc, this.bmsvc.addObserver, watcher, false);
  1153.  
  1154.         return watcher;
  1155.     }
  1156. };
  1157.  
  1158. function BookmarkWatcher(){
  1159.     this.lmsvc = Cc["@mozilla.org/browser/livemark-service;2"].
  1160.         getService(Ci.nsILivemarkService);
  1161.     this.mssvc = Cc["@mozilla.org/microsummary/service;1"].
  1162.         getService(Ci.nsIMicrosummaryService);
  1163. }
  1164.  
  1165. BookmarkWatcher.prototype = {
  1166.  
  1167.     lastModified: null,
  1168.  
  1169.     NotifyObservers: function(reason) {
  1170.         // Output Javascript milliseconds since 1970.
  1171.         var lm = Date.now();
  1172.         if (!this.lastModified || lm > this.lastModified) {
  1173.             this.lastModified = lm;
  1174.             var os = Cc["@mozilla.org/observer-service;1"]
  1175.                 .getService(Ci.nsIObserverService);
  1176.             os.notifyObservers(null, "foxmarks-datasourcechanged", 
  1177.                 lm + ";bookmarks");
  1178.         }
  1179.     },
  1180.  
  1181.     ////////////////////////////////////////////////////////////////////////////
  1182.     //
  1183.     // nsINavBookmarkObserver
  1184.  
  1185.     onItemAdded: function(itemId, folderId) { 
  1186.         if (!this.lmsvc.isLivemark(folderId)) {
  1187.             this.NotifyObservers("Added") 
  1188.         }
  1189.     },
  1190.  
  1191.     onItemRemoved: function(itemId, folderId) { 
  1192.         if (!this.lmsvc.isLivemark(folderId)) {
  1193.             this.NotifyObservers("Removed")
  1194.         }
  1195.     },
  1196.  
  1197.     onItemChanged: function(itemId, property) { 
  1198.         // Skip title change for Microsummaries
  1199.         if (this.mssvc.hasMicrosummary(itemId) && property == 'title')
  1200.             return;
  1201.  
  1202.         if (BookmarkDatasource.INTERESTING_PROPERTIES[property] ||
  1203.             BookmarkDatasource.MAP_ANNO_TO_NODE[property]) {
  1204.             this.NotifyObservers("Changed") 
  1205.         }
  1206.     },
  1207.  
  1208.     onItemMoved: function() { 
  1209.         this.NotifyObservers("Moved") 
  1210.     },
  1211.  
  1212.     onItemVisited: function() {},
  1213.     onBeginUpdateBatch: function() {},
  1214.     onEndUpdateBatch: function() {},
  1215.  
  1216.     QueryInterface: function(iid) {
  1217.         if (iid.equals(Ci.nsINavHistoryBatchCallback) ||
  1218.             iid.equals(Ci.nsINavBookmarkObserver))
  1219.             return this;
  1220.         throw Components.result.NS_ERROR_NO_INTERFACE;
  1221.     },
  1222. };
  1223.  
  1224.  
  1225.