toolkit/components/places/PlacesUtils.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     2 /* This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this
     4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 this.EXPORTED_SYMBOLS = [
     7   "PlacesUtils"
     8 , "PlacesAggregatedTransaction"
     9 , "PlacesCreateFolderTransaction"
    10 , "PlacesCreateBookmarkTransaction"
    11 , "PlacesCreateSeparatorTransaction"
    12 , "PlacesCreateLivemarkTransaction"
    13 , "PlacesMoveItemTransaction"
    14 , "PlacesRemoveItemTransaction"
    15 , "PlacesEditItemTitleTransaction"
    16 , "PlacesEditBookmarkURITransaction"
    17 , "PlacesSetItemAnnotationTransaction"
    18 , "PlacesSetPageAnnotationTransaction"
    19 , "PlacesEditBookmarkKeywordTransaction"
    20 , "PlacesEditBookmarkPostDataTransaction"
    21 , "PlacesEditItemDateAddedTransaction"
    22 , "PlacesEditItemLastModifiedTransaction"
    23 , "PlacesSortFolderByNameTransaction"
    24 , "PlacesTagURITransaction"
    25 , "PlacesUntagURITransaction"
    26 ];
    28 const Ci = Components.interfaces;
    29 const Cc = Components.classes;
    30 const Cr = Components.results;
    31 const Cu = Components.utils;
    33 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    35 XPCOMUtils.defineLazyModuleGetter(this, "Services",
    36                                   "resource://gre/modules/Services.jsm");
    38 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
    39                                   "resource://gre/modules/NetUtil.jsm");
    41 XPCOMUtils.defineLazyModuleGetter(this, "Task",
    42                                   "resource://gre/modules/Task.jsm");
    44 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
    45                                   "resource://gre/modules/Promise.jsm");
    47 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
    48                                   "resource://gre/modules/Deprecated.jsm");
    50 XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils",
    51                                   "resource://gre/modules/BookmarkJSONUtils.jsm");
    53 XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
    54                                   "resource://gre/modules/PlacesBackups.jsm");
    56 // The minimum amount of transactions before starting a batch. Usually we do
    57 // do incremental updates, a batch will cause views to completely
    58 // refresh instead.
    59 const MIN_TRANSACTIONS_FOR_BATCH = 5;
    61 #ifdef XP_MACOSX
    62 // On Mac OSX, the transferable system converts "\r\n" to "\n\n", where we
    63 // really just want "\n".
    64 const NEWLINE= "\n";
    65 #else
    66 // On other platforms, the transferable system converts "\r\n" to "\n".
    67 const NEWLINE = "\r\n";
    68 #endif
    70 function QI_node(aNode, aIID) {
    71   var result = null;
    72   try {
    73     result = aNode.QueryInterface(aIID);
    74   }
    75   catch (e) {
    76   }
    77   return result;
    78 }
    79 function asContainer(aNode) QI_node(aNode, Ci.nsINavHistoryContainerResultNode);
    80 function asQuery(aNode) QI_node(aNode, Ci.nsINavHistoryQueryResultNode);
    82 this.PlacesUtils = {
    83   // Place entries that are containers, e.g. bookmark folders or queries.
    84   TYPE_X_MOZ_PLACE_CONTAINER: "text/x-moz-place-container",
    85   // Place entries that are bookmark separators.
    86   TYPE_X_MOZ_PLACE_SEPARATOR: "text/x-moz-place-separator",
    87   // Place entries that are not containers or separators
    88   TYPE_X_MOZ_PLACE: "text/x-moz-place",
    89   // Place entries in shortcut url format (url\ntitle)
    90   TYPE_X_MOZ_URL: "text/x-moz-url",
    91   // Place entries formatted as HTML anchors
    92   TYPE_HTML: "text/html",
    93   // Place entries as raw URL text
    94   TYPE_UNICODE: "text/unicode",
    95   // Used to track the action that populated the clipboard.
    96   TYPE_X_MOZ_PLACE_ACTION: "text/x-moz-place-action",
    98   EXCLUDE_FROM_BACKUP_ANNO: "places/excludeFromBackup",
    99   LMANNO_FEEDURI: "livemark/feedURI",
   100   LMANNO_SITEURI: "livemark/siteURI",
   101   POST_DATA_ANNO: "bookmarkProperties/POSTData",
   102   READ_ONLY_ANNO: "placesInternal/READ_ONLY",
   103   CHARSET_ANNO: "URIProperties/characterSet",
   105   TOPIC_SHUTDOWN: "places-shutdown",
   106   TOPIC_INIT_COMPLETE: "places-init-complete",
   107   TOPIC_DATABASE_LOCKED: "places-database-locked",
   108   TOPIC_EXPIRATION_FINISHED: "places-expiration-finished",
   109   TOPIC_FEEDBACK_UPDATED: "places-autocomplete-feedback-updated",
   110   TOPIC_FAVICONS_EXPIRED: "places-favicons-expired",
   111   TOPIC_VACUUM_STARTING: "places-vacuum-starting",
   112   TOPIC_BOOKMARKS_RESTORE_BEGIN: "bookmarks-restore-begin",
   113   TOPIC_BOOKMARKS_RESTORE_SUCCESS: "bookmarks-restore-success",
   114   TOPIC_BOOKMARKS_RESTORE_FAILED: "bookmarks-restore-failed",
   116   asContainer: function(aNode) asContainer(aNode),
   117   asQuery: function(aNode) asQuery(aNode),
   119   endl: NEWLINE,
   121   /**
   122    * Makes a URI from a spec.
   123    * @param   aSpec
   124    *          The string spec of the URI
   125    * @returns A URI object for the spec.
   126    */
   127   _uri: function PU__uri(aSpec) {
   128     return NetUtil.newURI(aSpec);
   129   },
   131   /**
   132    * Wraps a string in a nsISupportsString wrapper.
   133    * @param   aString
   134    *          The string to wrap.
   135    * @returns A nsISupportsString object containing a string.
   136    */
   137   toISupportsString: function PU_toISupportsString(aString) {
   138     let s = Cc["@mozilla.org/supports-string;1"].
   139             createInstance(Ci.nsISupportsString);
   140     s.data = aString;
   141     return s;
   142   },
   144   getFormattedString: function PU_getFormattedString(key, params) {
   145     return bundle.formatStringFromName(key, params, params.length);
   146   },
   148   getString: function PU_getString(key) {
   149     return bundle.GetStringFromName(key);
   150   },
   152   /**
   153    * Determines whether or not a ResultNode is a Bookmark folder.
   154    * @param   aNode
   155    *          A result node
   156    * @returns true if the node is a Bookmark folder, false otherwise
   157    */
   158   nodeIsFolder: function PU_nodeIsFolder(aNode) {
   159     return (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER ||
   160             aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT);
   161   },
   163   /**
   164    * Determines whether or not a ResultNode represents a bookmarked URI.
   165    * @param   aNode
   166    *          A result node
   167    * @returns true if the node represents a bookmarked URI, false otherwise
   168    */
   169   nodeIsBookmark: function PU_nodeIsBookmark(aNode) {
   170     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI &&
   171            aNode.itemId != -1;
   172   },
   174   /**
   175    * Determines whether or not a ResultNode is a Bookmark separator.
   176    * @param   aNode
   177    *          A result node
   178    * @returns true if the node is a Bookmark separator, false otherwise
   179    */
   180   nodeIsSeparator: function PU_nodeIsSeparator(aNode) {
   181     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR;
   182   },
   184   /**
   185    * Determines whether or not a ResultNode is a URL item.
   186    * @param   aNode
   187    *          A result node
   188    * @returns true if the node is a URL item, false otherwise
   189    */
   190   nodeIsURI: function PU_nodeIsURI(aNode) {
   191     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
   192   },
   194   /**
   195    * Determines whether or not a ResultNode is a Query item.
   196    * @param   aNode
   197    *          A result node
   198    * @returns true if the node is a Query item, false otherwise
   199    */
   200   nodeIsQuery: function PU_nodeIsQuery(aNode) {
   201     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY;
   202   },
   204   /**
   205    * Generator for a node's ancestors.
   206    * @param aNode
   207    *        A result node
   208    */
   209   nodeAncestors: function PU_nodeAncestors(aNode) {
   210     let node = aNode.parent;
   211     while (node) {
   212       yield node;
   213       node = node.parent;
   214     }
   215   },
   217   /**
   218    * Cache array of read-only item IDs.
   219    *
   220    * The first time this property is called:
   221    * - the cache is filled with all ids with the RO annotation
   222    * - an annotation observer is added
   223    * - a shutdown observer is added
   224    *
   225    * When the annotation observer detects annotations added or
   226    * removed that are the RO annotation name, it adds/removes
   227    * the ids from the cache.
   228    *
   229    * At shutdown, the annotation and shutdown observers are removed.
   230    */
   231   get _readOnly() {
   232     // Add annotations observer.
   233     this.annotations.addObserver(this, false);
   234     this.registerShutdownFunction(function () {
   235       this.annotations.removeObserver(this);
   236     });
   238     var readOnly = this.annotations.getItemsWithAnnotation(this.READ_ONLY_ANNO);
   239     this.__defineGetter__("_readOnly", function() readOnly);
   240     return this._readOnly;
   241   },
   243   QueryInterface: XPCOMUtils.generateQI([
   244     Ci.nsIAnnotationObserver
   245   , Ci.nsIObserver
   246   , Ci.nsITransactionListener
   247   ]),
   249   _shutdownFunctions: [],
   250   registerShutdownFunction: function PU_registerShutdownFunction(aFunc)
   251   {
   252     // If this is the first registered function, add the shutdown observer.
   253     if (this._shutdownFunctions.length == 0) {
   254       Services.obs.addObserver(this, this.TOPIC_SHUTDOWN, false);
   255     }
   256     this._shutdownFunctions.push(aFunc);
   257   },
   259   //////////////////////////////////////////////////////////////////////////////
   260   //// nsIObserver
   261   observe: function PU_observe(aSubject, aTopic, aData)
   262   {
   263     switch (aTopic) {
   264       case this.TOPIC_SHUTDOWN:
   265         Services.obs.removeObserver(this, this.TOPIC_SHUTDOWN);
   266         while (this._shutdownFunctions.length > 0) {
   267           this._shutdownFunctions.shift().apply(this);
   268         }
   269         if (this._bookmarksServiceObserversQueue.length > 0) {
   270           // Since we are shutting down, there's no reason to add the observers.
   271           this._bookmarksServiceObserversQueue.length = 0;
   272         }
   273         break;
   274       case "bookmarks-service-ready":
   275         this._bookmarksServiceReady = true;
   276         while (this._bookmarksServiceObserversQueue.length > 0) {
   277           let observerInfo = this._bookmarksServiceObserversQueue.shift();
   278           this.bookmarks.addObserver(observerInfo.observer, observerInfo.weak);
   279         }
   280         break;
   281     }
   282   },
   284   //////////////////////////////////////////////////////////////////////////////
   285   //// nsIAnnotationObserver
   287   onItemAnnotationSet: function PU_onItemAnnotationSet(aItemId, aAnnotationName)
   288   {
   289     if (aAnnotationName == this.READ_ONLY_ANNO &&
   290         this._readOnly.indexOf(aItemId) == -1)
   291       this._readOnly.push(aItemId);
   292   },
   294   onItemAnnotationRemoved:
   295   function PU_onItemAnnotationRemoved(aItemId, aAnnotationName)
   296   {
   297     var index = this._readOnly.indexOf(aItemId);
   298     if (aAnnotationName == this.READ_ONLY_ANNO && index > -1)
   299       delete this._readOnly[index];
   300   },
   302   onPageAnnotationSet: function() {},
   303   onPageAnnotationRemoved: function() {},
   306   //////////////////////////////////////////////////////////////////////////////
   307   //// nsITransactionListener
   309   didDo: function PU_didDo(aManager, aTransaction, aDoResult)
   310   {
   311     updateCommandsOnActiveWindow();
   312   },
   314   didUndo: function PU_didUndo(aManager, aTransaction, aUndoResult)
   315   {
   316     updateCommandsOnActiveWindow();
   317   },
   319   didRedo: function PU_didRedo(aManager, aTransaction, aRedoResult)
   320   {
   321     updateCommandsOnActiveWindow();
   322   },
   324   didBeginBatch: function PU_didBeginBatch(aManager, aResult)
   325   {
   326     // A no-op transaction is pushed to the stack, in order to make safe and
   327     // easy to implement "Undo" an unknown number of transactions (including 0),
   328     // "above" beginBatch and endBatch. Otherwise,implementing Undo that way
   329     // head to dataloss: for example, if no changes were done in the
   330     // edit-item panel, the last transaction on the undo stack would be the
   331     // initial createItem transaction, or even worse, the batched editing of
   332     // some other item.
   333     // DO NOT MOVE this to the window scope, that would leak (bug 490068)!
   334     this.transactionManager.doTransaction({ doTransaction: function() {},
   335                                             undoTransaction: function() {},
   336                                             redoTransaction: function() {},
   337                                             isTransient: false,
   338                                             merge: function() { return false; }
   339                                           });
   340   },
   342   willDo: function PU_willDo() {},
   343   willUndo: function PU_willUndo() {},
   344   willRedo: function PU_willRedo() {},
   345   willBeginBatch: function PU_willBeginBatch() {},
   346   willEndBatch: function PU_willEndBatch() {},
   347   didEndBatch: function PU_didEndBatch() {},
   348   willMerge: function PU_willMerge() {},
   349   didMerge: function PU_didMerge() {},
   352   /**
   353    * Determines if a node is read only (children cannot be inserted, sometimes
   354    * they cannot be removed depending on the circumstance)
   355    * @param   aNode
   356    *          A result node
   357    * @returns true if the node is readonly, false otherwise
   358    */
   359   nodeIsReadOnly: function PU_nodeIsReadOnly(aNode) {
   360     let itemId = aNode.itemId;
   361     if (itemId != -1) {
   362       return this._readOnly.indexOf(itemId) != -1;
   363     }
   365     if (this.nodeIsQuery(aNode) &&
   366         asQuery(aNode).queryOptions.resultType !=
   367         Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_CONTENTS)
   368       return aNode.childrenReadOnly;
   369     return false;
   370   },
   372   /**
   373    * Determines whether or not a ResultNode is a host container.
   374    * @param   aNode
   375    *          A result node
   376    * @returns true if the node is a host container, false otherwise
   377    */
   378   nodeIsHost: function PU_nodeIsHost(aNode) {
   379     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
   380            aNode.parent &&
   381            asQuery(aNode.parent).queryOptions.resultType ==
   382              Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY;
   383   },
   385   /**
   386    * Determines whether or not a ResultNode is a day container.
   387    * @param   node
   388    *          A NavHistoryResultNode
   389    * @returns true if the node is a day container, false otherwise
   390    */
   391   nodeIsDay: function PU_nodeIsDay(aNode) {
   392     var resultType;
   393     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
   394            aNode.parent &&
   395            ((resultType = asQuery(aNode.parent).queryOptions.resultType) ==
   396                Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
   397              resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY);
   398   },
   400   /**
   401    * Determines whether or not a result-node is a tag container.
   402    * @param   aNode
   403    *          A result-node
   404    * @returns true if the node is a tag container, false otherwise
   405    */
   406   nodeIsTagQuery: function PU_nodeIsTagQuery(aNode) {
   407     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
   408            asQuery(aNode).queryOptions.resultType ==
   409              Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_CONTENTS;
   410   },
   412   /**
   413    * Determines whether or not a ResultNode is a container.
   414    * @param   aNode
   415    *          A result node
   416    * @returns true if the node is a container item, false otherwise
   417    */
   418   containerTypes: [Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
   419                    Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT,
   420                    Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY],
   421   nodeIsContainer: function PU_nodeIsContainer(aNode) {
   422     return this.containerTypes.indexOf(aNode.type) != -1;
   423   },
   425   /**
   426    * Determines whether or not a ResultNode is an history related container.
   427    * @param   node
   428    *          A result node
   429    * @returns true if the node is an history related container, false otherwise
   430    */
   431   nodeIsHistoryContainer: function PU_nodeIsHistoryContainer(aNode) {
   432     var resultType;
   433     return this.nodeIsQuery(aNode) &&
   434            ((resultType = asQuery(aNode).queryOptions.resultType) ==
   435               Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY ||
   436             resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
   437             resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY ||
   438             this.nodeIsDay(aNode) ||
   439             this.nodeIsHost(aNode));
   440   },
   442   /**
   443    * Determines whether or not a node is a readonly folder.
   444    * @param   aNode
   445    *          The node to test.
   446    * @returns true if the node is a readonly folder.
   447   */
   448   isReadonlyFolder: function(aNode) {
   449     return this.nodeIsFolder(aNode) &&
   450            this._readOnly.indexOf(asQuery(aNode).folderItemId) != -1;
   451   },
   453   /**
   454    * Gets the concrete item-id for the given node. Generally, this is just
   455    * node.itemId, but for folder-shortcuts that's node.folderItemId.
   456    */
   457   getConcreteItemId: function PU_getConcreteItemId(aNode) {
   458     if (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT)
   459       return asQuery(aNode).folderItemId;
   460     else if (PlacesUtils.nodeIsTagQuery(aNode)) {
   461       // RESULTS_AS_TAG_CONTENTS queries are similar to folder shortcuts
   462       // so we can still get the concrete itemId for them.
   463       var queries = aNode.getQueries();
   464       var folders = queries[0].getFolders();
   465       return folders[0];
   466     }
   467     return aNode.itemId;
   468   },
   470   /**
   471    * String-wraps a result node according to the rules of the specified
   472    * content type.
   473    * @param   aNode
   474    *          The Result node to wrap (serialize)
   475    * @param   aType
   476    *          The content type to serialize as
   477    * @param   [optional] aOverrideURI
   478    *          Used instead of the node's URI if provided.
   479    *          This is useful for wrapping a container as TYPE_X_MOZ_URL,
   480    *          TYPE_HTML or TYPE_UNICODE.
   481    * @return  A string serialization of the node
   482    */
   483   wrapNode: function PU_wrapNode(aNode, aType, aOverrideURI) {
   484     // when wrapping a node, we want all the items, even if the original
   485     // query options are excluding them.
   486     // this can happen when copying from the left hand pane of the bookmarks
   487     // organizer
   488     // @return [node, shouldClose]
   489     function convertNode(cNode) {
   490       if (PlacesUtils.nodeIsFolder(cNode) &&
   491           cNode.type != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT &&
   492           asQuery(cNode).queryOptions.excludeItems) {
   493         return [PlacesUtils.getFolderContents(cNode.itemId, false, true).root, true];
   494       }
   496       // If we didn't create our own query, do not alter the node's open state.
   497       return [cNode, false];
   498     }
   500     function gatherLivemarkUrl(aNode) {
   501       try {
   502         return PlacesUtils.annotations
   503                           .getItemAnnotation(aNode.itemId,
   504                                              PlacesUtils.LMANNO_SITEURI);
   505       } catch (ex) {
   506         return PlacesUtils.annotations
   507                           .getItemAnnotation(aNode.itemId,
   508                                              PlacesUtils.LMANNO_FEEDURI);
   509       }
   510     }
   512     function isLivemark(aNode) {
   513       return PlacesUtils.nodeIsFolder(aNode) &&
   514              PlacesUtils.annotations
   515                         .itemHasAnnotation(aNode.itemId,
   516                                            PlacesUtils.LMANNO_FEEDURI);
   517     }
   519     switch (aType) {
   520       case this.TYPE_X_MOZ_PLACE:
   521       case this.TYPE_X_MOZ_PLACE_SEPARATOR:
   522       case this.TYPE_X_MOZ_PLACE_CONTAINER: {
   523         let writer = {
   524           value: "",
   525           write: function PU_wrapNode__write(aStr, aLen) {
   526             this.value += aStr;
   527           }
   528         };
   530         let [node, shouldClose] = convertNode(aNode);
   531         this._serializeNodeAsJSONToOutputStream(node, writer);
   532         if (shouldClose)
   533           node.containerOpen = false;
   535         return writer.value;
   536       }
   537       case this.TYPE_X_MOZ_URL: {
   538         function gatherDataUrl(bNode) {
   539           if (isLivemark(bNode)) {
   540             return gatherLivemarkUrl(bNode) + NEWLINE + bNode.title;
   541           }
   543           if (PlacesUtils.nodeIsURI(bNode))
   544             return (aOverrideURI || bNode.uri) + NEWLINE + bNode.title;
   545           // ignore containers and separators - items without valid URIs
   546           return "";
   547         }
   549         let [node, shouldClose] = convertNode(aNode);
   550         let dataUrl = gatherDataUrl(node);
   551         if (shouldClose)
   552           node.containerOpen = false;
   554         return dataUrl;
   555       }
   556       case this.TYPE_HTML: {
   557         function gatherDataHtml(bNode) {
   558           function htmlEscape(s) {
   559             s = s.replace(/&/g, "&amp;");
   560             s = s.replace(/>/g, "&gt;");
   561             s = s.replace(/</g, "&lt;");
   562             s = s.replace(/"/g, "&quot;");
   563             s = s.replace(/'/g, "&apos;");
   564             return s;
   565           }
   566           // escape out potential HTML in the title
   567           let escapedTitle = bNode.title ? htmlEscape(bNode.title) : "";
   569           if (isLivemark(bNode)) {
   570             return "<A HREF=\"" + gatherLivemarkUrl(bNode) + "\">" + escapedTitle + "</A>" + NEWLINE;
   571           }
   573           if (PlacesUtils.nodeIsContainer(bNode)) {
   574             asContainer(bNode);
   575             let wasOpen = bNode.containerOpen;
   576             if (!wasOpen)
   577               bNode.containerOpen = true;
   579             let childString = "<DL><DT>" + escapedTitle + "</DT>" + NEWLINE;
   580             let cc = bNode.childCount;
   581             for (let i = 0; i < cc; ++i)
   582               childString += "<DD>"
   583                              + NEWLINE
   584                              + gatherDataHtml(bNode.getChild(i))
   585                              + "</DD>"
   586                              + NEWLINE;
   587             bNode.containerOpen = wasOpen;
   588             return childString + "</DL>" + NEWLINE;
   589           }
   590           if (PlacesUtils.nodeIsURI(bNode))
   591             return "<A HREF=\"" + bNode.uri + "\">" + escapedTitle + "</A>" + NEWLINE;
   592           if (PlacesUtils.nodeIsSeparator(bNode))
   593             return "<HR>" + NEWLINE;
   594           return "";
   595         }
   597         let [node, shouldClose] = convertNode(aNode);
   598         let dataHtml = gatherDataHtml(node);
   599         if (shouldClose)
   600           node.containerOpen = false;
   602         return dataHtml;
   603       }
   604     }
   606     // Otherwise, we wrap as TYPE_UNICODE.
   607     function gatherDataText(bNode) {
   608       if (isLivemark(bNode)) {
   609         return gatherLivemarkUrl(bNode);
   610       }
   612       if (PlacesUtils.nodeIsContainer(bNode)) {
   613         asContainer(bNode);
   614         let wasOpen = bNode.containerOpen;
   615         if (!wasOpen)
   616           bNode.containerOpen = true;
   618         let childString = bNode.title + NEWLINE;
   619         let cc = bNode.childCount;
   620         for (let i = 0; i < cc; ++i) {
   621           let child = bNode.getChild(i);
   622           let suffix = i < (cc - 1) ? NEWLINE : "";
   623           childString += gatherDataText(child) + suffix;
   624         }
   625         bNode.containerOpen = wasOpen;
   626         return childString;
   627       }
   628       if (PlacesUtils.nodeIsURI(bNode))
   629         return (aOverrideURI || bNode.uri);
   630       if (PlacesUtils.nodeIsSeparator(bNode))
   631         return "--------------------";
   632       return "";
   633     }
   635     let [node, shouldClose] = convertNode(aNode);
   636     let dataText = gatherDataText(node);
   637     // Convert node could pass an open container node.
   638     if (shouldClose)
   639       node.containerOpen = false;
   641     return dataText;
   642   },
   644   /**
   645    * Unwraps data from the Clipboard or the current Drag Session.
   646    * @param   blob
   647    *          A blob (string) of data, in some format we potentially know how
   648    *          to parse.
   649    * @param   type
   650    *          The content type of the blob.
   651    * @returns An array of objects representing each item contained by the source.
   652    */
   653   unwrapNodes: function PU_unwrapNodes(blob, type) {
   654     // We split on "\n"  because the transferable system converts "\r\n" to "\n"
   655     var nodes = [];
   656     switch(type) {
   657       case this.TYPE_X_MOZ_PLACE:
   658       case this.TYPE_X_MOZ_PLACE_SEPARATOR:
   659       case this.TYPE_X_MOZ_PLACE_CONTAINER:
   660         nodes = JSON.parse("[" + blob + "]");
   661         break;
   662       case this.TYPE_X_MOZ_URL:
   663         var parts = blob.split("\n");
   664         // data in this type has 2 parts per entry, so if there are fewer
   665         // than 2 parts left, the blob is malformed and we should stop
   666         // but drag and drop of files from the shell has parts.length = 1
   667         if (parts.length != 1 && parts.length % 2)
   668           break;
   669         for (var i = 0; i < parts.length; i=i+2) {
   670           var uriString = parts[i];
   671           var titleString = "";
   672           if (parts.length > i+1)
   673             titleString = parts[i+1];
   674           else {
   675             // for drag and drop of files, try to use the leafName as title
   676             try {
   677               titleString = this._uri(uriString).QueryInterface(Ci.nsIURL)
   678                                 .fileName;
   679             }
   680             catch (e) {}
   681           }
   682           // note:  this._uri() will throw if uriString is not a valid URI
   683           if (this._uri(uriString)) {
   684             nodes.push({ uri: uriString,
   685                          title: titleString ? titleString : uriString ,
   686                          type: this.TYPE_X_MOZ_URL });
   687           }
   688         }
   689         break;
   690       case this.TYPE_UNICODE:
   691         var parts = blob.split("\n");
   692         for (var i = 0; i < parts.length; i++) {
   693           var uriString = parts[i];
   694           // text/uri-list is converted to TYPE_UNICODE but it could contain
   695           // comments line prepended by #, we should skip them
   696           if (uriString.substr(0, 1) == '\x23')
   697             continue;
   698           // note: this._uri() will throw if uriString is not a valid URI
   699           if (uriString != "" && this._uri(uriString))
   700             nodes.push({ uri: uriString,
   701                          title: uriString,
   702                          type: this.TYPE_X_MOZ_URL });
   703         }
   704         break;
   705       default:
   706         throw Cr.NS_ERROR_INVALID_ARG;
   707     }
   708     return nodes;
   709   },
   711   /**
   712    * Generates a nsINavHistoryResult for the contents of a folder.
   713    * @param   folderId
   714    *          The folder to open
   715    * @param   [optional] excludeItems
   716    *          True to hide all items (individual bookmarks). This is used on
   717    *          the left places pane so you just get a folder hierarchy.
   718    * @param   [optional] expandQueries
   719    *          True to make query items expand as new containers. For managing,
   720    *          you want this to be false, for menus and such, you want this to
   721    *          be true.
   722    * @returns A nsINavHistoryResult containing the contents of the
   723    *          folder. The result.root is guaranteed to be open.
   724    */
   725   getFolderContents:
   726   function PU_getFolderContents(aFolderId, aExcludeItems, aExpandQueries) {
   727     var query = this.history.getNewQuery();
   728     query.setFolders([aFolderId], 1);
   729     var options = this.history.getNewQueryOptions();
   730     options.excludeItems = aExcludeItems;
   731     options.expandQueries = aExpandQueries;
   733     var result = this.history.executeQuery(query, options);
   734     result.root.containerOpen = true;
   735     return result;
   736   },
   738   /**
   739    * Fetch all annotations for a URI, including all properties of each
   740    * annotation which would be required to recreate it.
   741    * @param aURI
   742    *        The URI for which annotations are to be retrieved.
   743    * @return Array of objects, each containing the following properties:
   744    *         name, flags, expires, value
   745    */
   746   getAnnotationsForURI: function PU_getAnnotationsForURI(aURI) {
   747     var annosvc = this.annotations;
   748     var annos = [], val = null;
   749     var annoNames = annosvc.getPageAnnotationNames(aURI);
   750     for (var i = 0; i < annoNames.length; i++) {
   751       var flags = {}, exp = {}, storageType = {};
   752       annosvc.getPageAnnotationInfo(aURI, annoNames[i], flags, exp, storageType);
   753       val = annosvc.getPageAnnotation(aURI, annoNames[i]);
   754       annos.push({name: annoNames[i],
   755                   flags: flags.value,
   756                   expires: exp.value,
   757                   value: val});
   758     }
   759     return annos;
   760   },
   762   /**
   763    * Fetch all annotations for an item, including all properties of each
   764    * annotation which would be required to recreate it.
   765    * @param aItemId
   766    *        The identifier of the itme for which annotations are to be
   767    *        retrieved.
   768    * @return Array of objects, each containing the following properties:
   769    *         name, flags, expires, mimeType, type, value
   770    */
   771   getAnnotationsForItem: function PU_getAnnotationsForItem(aItemId) {
   772     var annosvc = this.annotations;
   773     var annos = [], val = null;
   774     var annoNames = annosvc.getItemAnnotationNames(aItemId);
   775     for (var i = 0; i < annoNames.length; i++) {
   776       var flags = {}, exp = {}, storageType = {};
   777       annosvc.getItemAnnotationInfo(aItemId, annoNames[i], flags, exp, storageType);
   778       val = annosvc.getItemAnnotation(aItemId, annoNames[i]);
   779       annos.push({name: annoNames[i],
   780                   flags: flags.value,
   781                   expires: exp.value,
   782                   value: val});
   783     }
   784     return annos;
   785   },
   787   /**
   788    * Annotate a URI with a batch of annotations.
   789    * @param aURI
   790    *        The URI for which annotations are to be set.
   791    * @param aAnnotations
   792    *        Array of objects, each containing the following properties:
   793    *        name, flags, expires.
   794    *        If the value for an annotation is not set it will be removed.
   795    */
   796   setAnnotationsForURI: function PU_setAnnotationsForURI(aURI, aAnnos) {
   797     var annosvc = this.annotations;
   798     aAnnos.forEach(function(anno) {
   799       if (anno.value === undefined || anno.value === null) {
   800         annosvc.removePageAnnotation(aURI, anno.name);
   801       }
   802       else {
   803         let flags = ("flags" in anno) ? anno.flags : 0;
   804         let expires = ("expires" in anno) ?
   805           anno.expires : Ci.nsIAnnotationService.EXPIRE_NEVER;
   806         annosvc.setPageAnnotation(aURI, anno.name, anno.value, flags, expires);
   807       }
   808     });
   809   },
   811   /**
   812    * Annotate an item with a batch of annotations.
   813    * @param aItemId
   814    *        The identifier of the item for which annotations are to be set
   815    * @param aAnnotations
   816    *        Array of objects, each containing the following properties:
   817    *        name, flags, expires.
   818    *        If the value for an annotation is not set it will be removed.
   819    */
   820   setAnnotationsForItem: function PU_setAnnotationsForItem(aItemId, aAnnos) {
   821     var annosvc = this.annotations;
   823     aAnnos.forEach(function(anno) {
   824       if (anno.value === undefined || anno.value === null) {
   825         annosvc.removeItemAnnotation(aItemId, anno.name);
   826       }
   827       else {
   828         let flags = ("flags" in anno) ? anno.flags : 0;
   829         let expires = ("expires" in anno) ?
   830           anno.expires : Ci.nsIAnnotationService.EXPIRE_NEVER;
   831         annosvc.setItemAnnotation(aItemId, anno.name, anno.value, flags,
   832                                   expires);
   833       }
   834     });
   835   },
   837   // Identifier getters for special folders.
   838   // You should use these everywhere PlacesUtils is available to avoid XPCOM
   839   // traversal just to get roots' ids.
   840   get placesRootId() {
   841     delete this.placesRootId;
   842     return this.placesRootId = this.bookmarks.placesRoot;
   843   },
   845   get bookmarksMenuFolderId() {
   846     delete this.bookmarksMenuFolderId;
   847     return this.bookmarksMenuFolderId = this.bookmarks.bookmarksMenuFolder;
   848   },
   850   get toolbarFolderId() {
   851     delete this.toolbarFolderId;
   852     return this.toolbarFolderId = this.bookmarks.toolbarFolder;
   853   },
   855   get tagsFolderId() {
   856     delete this.tagsFolderId;
   857     return this.tagsFolderId = this.bookmarks.tagsFolder;
   858   },
   860   get unfiledBookmarksFolderId() {
   861     delete this.unfiledBookmarksFolderId;
   862     return this.unfiledBookmarksFolderId = this.bookmarks.unfiledBookmarksFolder;
   863   },
   865   /**
   866    * Checks if aItemId is a root.
   867    *
   868    *   @param aItemId
   869    *          item id to look for.
   870    *   @returns true if aItemId is a root, false otherwise.
   871    */
   872   isRootItem: function PU_isRootItem(aItemId) {
   873     return aItemId == PlacesUtils.bookmarksMenuFolderId ||
   874            aItemId == PlacesUtils.toolbarFolderId ||
   875            aItemId == PlacesUtils.unfiledBookmarksFolderId ||
   876            aItemId == PlacesUtils.tagsFolderId ||
   877            aItemId == PlacesUtils.placesRootId;
   878   },
   880   /**
   881    * Set the POST data associated with a bookmark, if any.
   882    * Used by POST keywords.
   883    *   @param aBookmarkId
   884    *   @returns string of POST data
   885    */
   886   setPostDataForBookmark: function PU_setPostDataForBookmark(aBookmarkId, aPostData) {
   887     const annos = this.annotations;
   888     if (aPostData)
   889       annos.setItemAnnotation(aBookmarkId, this.POST_DATA_ANNO, aPostData, 
   890                               0, Ci.nsIAnnotationService.EXPIRE_NEVER);
   891     else if (annos.itemHasAnnotation(aBookmarkId, this.POST_DATA_ANNO))
   892       annos.removeItemAnnotation(aBookmarkId, this.POST_DATA_ANNO);
   893   },
   895   /**
   896    * Get the POST data associated with a bookmark, if any.
   897    * @param aBookmarkId
   898    * @returns string of POST data if set for aBookmarkId. null otherwise.
   899    */
   900   getPostDataForBookmark: function PU_getPostDataForBookmark(aBookmarkId) {
   901     const annos = this.annotations;
   902     if (annos.itemHasAnnotation(aBookmarkId, this.POST_DATA_ANNO))
   903       return annos.getItemAnnotation(aBookmarkId, this.POST_DATA_ANNO);
   905     return null;
   906   },
   908   /**
   909    * Get the URI (and any associated POST data) for a given keyword.
   910    * @param aKeyword string keyword
   911    * @returns an array containing a string URL and a string of POST data
   912    */
   913   getURLAndPostDataForKeyword: function PU_getURLAndPostDataForKeyword(aKeyword) {
   914     var url = null, postdata = null;
   915     try {
   916       var uri = this.bookmarks.getURIForKeyword(aKeyword);
   917       if (uri) {
   918         url = uri.spec;
   919         var bookmarks = this.bookmarks.getBookmarkIdsForURI(uri);
   920         for (let i = 0; i < bookmarks.length; i++) {
   921           var bookmark = bookmarks[i];
   922           var kw = this.bookmarks.getKeywordForBookmark(bookmark);
   923           if (kw == aKeyword) {
   924             postdata = this.getPostDataForBookmark(bookmark);
   925             break;
   926           }
   927         }
   928       }
   929     } catch(ex) {}
   930     return [url, postdata];
   931   },
   933   /**
   934    * Get all bookmarks for a URL, excluding items under tags.
   935    */
   936   getBookmarksForURI:
   937   function PU_getBookmarksForURI(aURI) {
   938     var bmkIds = this.bookmarks.getBookmarkIdsForURI(aURI);
   940     // filter the ids list
   941     return bmkIds.filter(function(aID) {
   942       var parentId = this.bookmarks.getFolderIdForItem(aID);
   943       var grandparentId = this.bookmarks.getFolderIdForItem(parentId);
   944       // item under a tag container
   945       if (grandparentId == this.tagsFolderId)
   946         return false;
   947       return true;
   948     }, this);
   949   },
   951   /**
   952    * Get the most recently added/modified bookmark for a URL, excluding items
   953    * under tags.
   954    *
   955    * @param aURI
   956    *        nsIURI of the page we will look for.
   957    * @returns itemId of the found bookmark, or -1 if nothing is found.
   958    */
   959   getMostRecentBookmarkForURI:
   960   function PU_getMostRecentBookmarkForURI(aURI) {
   961     var bmkIds = this.bookmarks.getBookmarkIdsForURI(aURI);
   962     for (var i = 0; i < bmkIds.length; i++) {
   963       // Find the first folder which isn't a tag container
   964       var itemId = bmkIds[i];
   965       var parentId = this.bookmarks.getFolderIdForItem(itemId);
   966       // Optimization: if this is a direct child of a root we don't need to
   967       // check if its grandparent is a tag.
   968       if (parentId == this.unfiledBookmarksFolderId ||
   969           parentId == this.toolbarFolderId ||
   970           parentId == this.bookmarksMenuFolderId)
   971         return itemId;
   973       var grandparentId = this.bookmarks.getFolderIdForItem(parentId);
   974       if (grandparentId != this.tagsFolderId)
   975         return itemId;
   976     }
   977     return -1;
   978   },
   980   /**
   981    * Returns a nsNavHistoryContainerResultNode with forced excludeItems and
   982    * expandQueries.
   983    * @param   aNode
   984    *          The node to convert
   985    * @param   [optional] excludeItems
   986    *          True to hide all items (individual bookmarks). This is used on
   987    *          the left places pane so you just get a folder hierarchy.
   988    * @param   [optional] expandQueries
   989    *          True to make query items expand as new containers. For managing,
   990    *          you want this to be false, for menus and such, you want this to
   991    *          be true.
   992    * @returns A nsINavHistoryContainerResultNode containing the unfiltered
   993    *          contents of the container.
   994    * @note    The returned container node could be open or closed, we don't
   995    *          guarantee its status.
   996    */
   997   getContainerNodeWithOptions:
   998   function PU_getContainerNodeWithOptions(aNode, aExcludeItems, aExpandQueries) {
   999     if (!this.nodeIsContainer(aNode))
  1000       throw Cr.NS_ERROR_INVALID_ARG;
  1002     // excludeItems is inherited by child containers in an excludeItems view.
  1003     var excludeItems = asQuery(aNode).queryOptions.excludeItems ||
  1004                        asQuery(aNode.parentResult.root).queryOptions.excludeItems;
  1005     // expandQueries is inherited by child containers in an expandQueries view.
  1006     var expandQueries = asQuery(aNode).queryOptions.expandQueries &&
  1007                         asQuery(aNode.parentResult.root).queryOptions.expandQueries;
  1009     // If our options are exactly what we expect, directly return the node.
  1010     if (excludeItems == aExcludeItems && expandQueries == aExpandQueries)
  1011       return aNode;
  1013     // Otherwise, get contents manually.
  1014     var queries = {}, options = {};
  1015     this.history.queryStringToQueries(aNode.uri, queries, {}, options);
  1016     options.value.excludeItems = aExcludeItems;
  1017     options.value.expandQueries = aExpandQueries;
  1018     return this.history.executeQueries(queries.value,
  1019                                        queries.value.length,
  1020                                        options.value).root;
  1021   },
  1023   /**
  1024    * Returns true if a container has uri nodes in its first level.
  1025    * Has better performance than (getURLsForContainerNode(node).length > 0).
  1026    * @param aNode
  1027    *        The container node to search through.
  1028    * @returns true if the node contains uri nodes, false otherwise.
  1029    */
  1030   hasChildURIs: function PU_hasChildURIs(aNode) {
  1031     if (!this.nodeIsContainer(aNode))
  1032       return false;
  1034     let root = this.getContainerNodeWithOptions(aNode, false, true);
  1035     let result = root.parentResult;
  1036     let didSuppressNotifications = false;
  1037     let wasOpen = root.containerOpen;
  1038     if (!wasOpen) {
  1039       didSuppressNotifications = result.suppressNotifications;
  1040       if (!didSuppressNotifications)
  1041         result.suppressNotifications = true;
  1043       root.containerOpen = true;
  1046     let found = false;
  1047     for (let i = 0; i < root.childCount && !found; i++) {
  1048       let child = root.getChild(i);
  1049       if (this.nodeIsURI(child))
  1050         found = true;
  1053     if (!wasOpen) {
  1054       root.containerOpen = false;
  1055       if (!didSuppressNotifications)
  1056         result.suppressNotifications = false;
  1058     return found;
  1059   },
  1061   /**
  1062    * Returns an array containing all the uris in the first level of the
  1063    * passed in container.
  1064    * If you only need to know if the node contains uris, use hasChildURIs.
  1065    * @param aNode
  1066    *        The container node to search through
  1067    * @returns array of uris in the first level of the container.
  1068    */
  1069   getURLsForContainerNode: function PU_getURLsForContainerNode(aNode) {
  1070     let urls = [];
  1071     if (!this.nodeIsContainer(aNode))
  1072       return urls;
  1074     let root = this.getContainerNodeWithOptions(aNode, false, true);
  1075     let result = root.parentResult;
  1076     let wasOpen = root.containerOpen;
  1077     let didSuppressNotifications = false;
  1078     if (!wasOpen) {
  1079       didSuppressNotifications = result.suppressNotifications;
  1080       if (!didSuppressNotifications)
  1081         result.suppressNotifications = true;
  1083       root.containerOpen = true;
  1086     for (let i = 0; i < root.childCount; ++i) {
  1087       let child = root.getChild(i);
  1088       if (this.nodeIsURI(child))
  1089         urls.push({uri: child.uri, isBookmark: this.nodeIsBookmark(child)});
  1092     if (!wasOpen) {
  1093       root.containerOpen = false;
  1094       if (!didSuppressNotifications)
  1095         result.suppressNotifications = false;
  1097     return urls;
  1098   },
  1100   /**
  1101    * Serializes the given node (and all its descendents) as JSON
  1102    * and writes the serialization to the given output stream.
  1104    * @param   aNode
  1105    *          An nsINavHistoryResultNode
  1106    * @param   aStream
  1107    *          An nsIOutputStream. NOTE: it only uses the write(str, len)
  1108    *          method of nsIOutputStream. The caller is responsible for
  1109    *          closing the stream.
  1110    */
  1111   _serializeNodeAsJSONToOutputStream: function (aNode, aStream) {
  1112     function addGenericProperties(aPlacesNode, aJSNode) {
  1113       aJSNode.title = aPlacesNode.title;
  1114       aJSNode.id = aPlacesNode.itemId;
  1115       if (aJSNode.id != -1) {
  1116         var parent = aPlacesNode.parent;
  1117         if (parent) {
  1118           aJSNode.parent = parent.itemId;
  1119           aJSNode.parentReadOnly = PlacesUtils.nodeIsReadOnly(parent);
  1121         var dateAdded = aPlacesNode.dateAdded;
  1122         if (dateAdded)
  1123           aJSNode.dateAdded = dateAdded;
  1124         var lastModified = aPlacesNode.lastModified;
  1125         if (lastModified)
  1126           aJSNode.lastModified = lastModified;
  1128         // XXX need a hasAnnos api
  1129         var annos = [];
  1130         try {
  1131           annos = PlacesUtils.getAnnotationsForItem(aJSNode.id).filter(function(anno) {
  1132             // XXX should whitelist this instead, w/ a pref for
  1133             // backup/restore of non-whitelisted annos
  1134             // XXX causes JSON encoding errors, so utf-8 encode
  1135             //anno.value = unescape(encodeURIComponent(anno.value));
  1136             if (anno.name == PlacesUtils.LMANNO_FEEDURI)
  1137               aJSNode.livemark = 1;
  1138             return true;
  1139           });
  1140         } catch(ex) {}
  1141         if (annos.length != 0)
  1142           aJSNode.annos = annos;
  1144       // XXXdietrich - store annos for non-bookmark items
  1147     function addURIProperties(aPlacesNode, aJSNode) {
  1148       aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE;
  1149       aJSNode.uri = aPlacesNode.uri;
  1150       if (aJSNode.id && aJSNode.id != -1) {
  1151         // harvest bookmark-specific properties
  1152         var keyword = PlacesUtils.bookmarks.getKeywordForBookmark(aJSNode.id);
  1153         if (keyword)
  1154           aJSNode.keyword = keyword;
  1157       if (aPlacesNode.tags)
  1158         aJSNode.tags = aPlacesNode.tags;
  1160       // last character-set
  1161       var uri = PlacesUtils._uri(aPlacesNode.uri);
  1162       try {
  1163         var lastCharset = PlacesUtils.annotations.getPageAnnotation(
  1164                             uri, PlacesUtils.CHARSET_ANNO);
  1165         aJSNode.charset = lastCharset;
  1166       } catch (e) {}
  1169     function addSeparatorProperties(aPlacesNode, aJSNode) {
  1170       aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
  1173     function addContainerProperties(aPlacesNode, aJSNode) {
  1174       var concreteId = PlacesUtils.getConcreteItemId(aPlacesNode);
  1175       if (concreteId != -1) {
  1176         // This is a bookmark or a tag container.
  1177         if (PlacesUtils.nodeIsQuery(aPlacesNode) ||
  1178             concreteId != aPlacesNode.itemId) {
  1179           aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE;
  1180           aJSNode.uri = aPlacesNode.uri;
  1181           // folder shortcut
  1182           aJSNode.concreteId = concreteId;
  1184         else { // Bookmark folder or a shortcut we should convert to folder.
  1185           aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
  1187           // Mark root folders.
  1188           if (aJSNode.id == PlacesUtils.placesRootId)
  1189             aJSNode.root = "placesRoot";
  1190           else if (aJSNode.id == PlacesUtils.bookmarksMenuFolderId)
  1191             aJSNode.root = "bookmarksMenuFolder";
  1192           else if (aJSNode.id == PlacesUtils.tagsFolderId)
  1193             aJSNode.root = "tagsFolder";
  1194           else if (aJSNode.id == PlacesUtils.unfiledBookmarksFolderId)
  1195             aJSNode.root = "unfiledBookmarksFolder";
  1196           else if (aJSNode.id == PlacesUtils.toolbarFolderId)
  1197             aJSNode.root = "toolbarFolder";
  1200       else {
  1201         // This is a grouped container query, generated on the fly.
  1202         aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE;
  1203         aJSNode.uri = aPlacesNode.uri;
  1207     function appendConvertedComplexNode(aNode, aSourceNode, aArray) {
  1208       var repr = {};
  1210       for (let [name, value] in Iterator(aNode))
  1211         repr[name] = value;
  1213       // write child nodes
  1214       var children = repr.children = [];
  1215       if (!aNode.livemark) {
  1216         asContainer(aSourceNode);
  1217         var wasOpen = aSourceNode.containerOpen;
  1218         if (!wasOpen)
  1219           aSourceNode.containerOpen = true;
  1220         var cc = aSourceNode.childCount;
  1221         for (var i = 0; i < cc; ++i) {
  1222           var childNode = aSourceNode.getChild(i);
  1223           appendConvertedNode(aSourceNode.getChild(i), i, children);
  1225         if (!wasOpen)
  1226           aSourceNode.containerOpen = false;
  1229       aArray.push(repr);
  1230       return true;
  1233     function appendConvertedNode(bNode, aIndex, aArray) {
  1234       var node = {};
  1236       // set index in order received
  1237       // XXX handy shortcut, but are there cases where we don't want
  1238       // to export using the sorting provided by the query?
  1239       if (aIndex)
  1240         node.index = aIndex;
  1242       addGenericProperties(bNode, node);
  1244       var parent = bNode.parent;
  1245       var grandParent = parent ? parent.parent : null;
  1246       if (grandParent)
  1247         node.grandParentId = grandParent.itemId;
  1249       if (PlacesUtils.nodeIsURI(bNode)) {
  1250         // Tag root accept only folder nodes
  1251         if (parent && parent.itemId == PlacesUtils.tagsFolderId)
  1252           return false;
  1254         // Check for url validity, since we can't halt while writing a backup.
  1255         // This will throw if we try to serialize an invalid url and it does
  1256         // not make sense saving a wrong or corrupt uri node.
  1257         try {
  1258           PlacesUtils._uri(bNode.uri);
  1259         } catch (ex) {
  1260           return false;
  1263         addURIProperties(bNode, node);
  1265       else if (PlacesUtils.nodeIsContainer(bNode)) {
  1266         // Tag containers accept only uri nodes
  1267         if (grandParent && grandParent.itemId == PlacesUtils.tagsFolderId)
  1268           return false;
  1270         addContainerProperties(bNode, node);
  1272       else if (PlacesUtils.nodeIsSeparator(bNode)) {
  1273         // Tag root accept only folder nodes
  1274         // Tag containers accept only uri nodes
  1275         if ((parent && parent.itemId == PlacesUtils.tagsFolderId) ||
  1276             (grandParent && grandParent.itemId == PlacesUtils.tagsFolderId))
  1277           return false;
  1279         addSeparatorProperties(bNode, node);
  1282       if (!node.feedURI && node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER)
  1283         return appendConvertedComplexNode(node, bNode, aArray);
  1285       aArray.push(node);
  1286       return true;
  1289     // serialize to stream
  1290     var array = [];
  1291     if (appendConvertedNode(aNode, null, array)) {
  1292       var json = JSON.stringify(array[0]);
  1293       aStream.write(json, json.length);
  1295     else {
  1296       throw Cr.NS_ERROR_UNEXPECTED;
  1298   },
  1300   /**
  1301    * Given a uri returns list of itemIds associated to it.
  1303    * @param aURI
  1304    *        nsIURI or spec of the page.
  1305    * @param aCallback
  1306    *        Function to be called when done.
  1307    *        The function will receive an array of itemIds associated to aURI and
  1308    *        aURI itself.
  1310    * @return A object with a .cancel() method allowing to cancel the request.
  1312    * @note Children of live bookmarks folders are excluded. The callback function is
  1313    *       not invoked if the request is cancelled or hits an error.
  1314    */
  1315   asyncGetBookmarkIds: function PU_asyncGetBookmarkIds(aURI, aCallback)
  1317     if (!this._asyncGetBookmarksStmt) {
  1318       let db = this.history.DBConnection;
  1319       this._asyncGetBookmarksStmt = db.createAsyncStatement(
  1320         "SELECT b.id "
  1321       + "FROM moz_bookmarks b "
  1322       + "JOIN moz_places h on h.id = b.fk "
  1323       + "WHERE h.url = :url "
  1324       );
  1325       this.registerShutdownFunction(function () {
  1326         this._asyncGetBookmarksStmt.finalize();
  1327       });
  1330     let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
  1331     this._asyncGetBookmarksStmt.params.url = url;
  1333     // Storage does not guarantee that invoking cancel() on a statement
  1334     // will cause a REASON_CANCELED.  Thus we wrap the statement.
  1335     let stmt = new AsyncStatementCancelWrapper(this._asyncGetBookmarksStmt);
  1336     return stmt.executeAsync({
  1337       _callback: aCallback,
  1338       _itemIds: [],
  1339       handleResult: function(aResultSet) {
  1340         for (let row; (row = aResultSet.getNextRow());) {
  1341           this._itemIds.push(row.getResultByIndex(0));
  1343       },
  1344       handleCompletion: function(aReason)
  1346         if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
  1347           this._callback(this._itemIds, aURI);
  1350     });
  1351   },
  1353   /**
  1354    * Lazily adds a bookmarks observer, waiting for the bookmarks service to be
  1355    * alive before registering the observer.  This is especially useful in the
  1356    * startup path, to avoid initializing the service just to add an observer.
  1358    * @param aObserver
  1359    *        Object implementing nsINavBookmarkObserver
  1360    * @param [optional]aWeakOwner
  1361    *        Whether to use weak ownership.
  1363    * @note Correct functionality of lazy observers relies on the fact Places
  1364    *       notifies categories before real observers, and uses
  1365    *       PlacesCategoriesStarter component to kick-off the registration.
  1366    */
  1367   _bookmarksServiceReady: false,
  1368   _bookmarksServiceObserversQueue: [],
  1369   addLazyBookmarkObserver:
  1370   function PU_addLazyBookmarkObserver(aObserver, aWeakOwner) {
  1371     if (this._bookmarksServiceReady) {
  1372       this.bookmarks.addObserver(aObserver, aWeakOwner === true);
  1373       return;
  1375     this._bookmarksServiceObserversQueue.push({ observer: aObserver,
  1376                                                 weak: aWeakOwner === true });
  1377   },
  1379   /**
  1380    * Removes a bookmarks observer added through addLazyBookmarkObserver.
  1382    * @param aObserver
  1383    *        Object implementing nsINavBookmarkObserver
  1384    */
  1385   removeLazyBookmarkObserver:
  1386   function PU_removeLazyBookmarkObserver(aObserver) {
  1387     if (this._bookmarksServiceReady) {
  1388       this.bookmarks.removeObserver(aObserver);
  1389       return;
  1391     let index = -1;
  1392     for (let i = 0;
  1393          i < this._bookmarksServiceObserversQueue.length && index == -1; i++) {
  1394       if (this._bookmarksServiceObserversQueue[i].observer === aObserver)
  1395         index = i;
  1397     if (index != -1) {
  1398       this._bookmarksServiceObserversQueue.splice(index, 1);
  1400   },
  1402   /**
  1403    * Sets the character-set for a URI.
  1405    * @param aURI nsIURI
  1406    * @param aCharset character-set value.
  1407    * @return {Promise}
  1408    */
  1409   setCharsetForURI: function PU_setCharsetForURI(aURI, aCharset) {
  1410     let deferred = Promise.defer();
  1412     // Delaying to catch issues with asynchronous behavior while waiting
  1413     // to implement asynchronous annotations in bug 699844.
  1414     Services.tm.mainThread.dispatch(function() {
  1415       if (aCharset && aCharset.length > 0) {
  1416         PlacesUtils.annotations.setPageAnnotation(
  1417           aURI, PlacesUtils.CHARSET_ANNO, aCharset, 0,
  1418           Ci.nsIAnnotationService.EXPIRE_NEVER);
  1419       } else {
  1420         PlacesUtils.annotations.removePageAnnotation(
  1421           aURI, PlacesUtils.CHARSET_ANNO);
  1423       deferred.resolve();
  1424     }, Ci.nsIThread.DISPATCH_NORMAL);
  1426     return deferred.promise;
  1427   },
  1429   /**
  1430    * Gets the last saved character-set for a URI.
  1432    * @param aURI nsIURI
  1433    * @return {Promise}
  1434    * @resolve a character-set or null.
  1435    */
  1436   getCharsetForURI: function PU_getCharsetForURI(aURI) {
  1437     let deferred = Promise.defer();
  1439     Services.tm.mainThread.dispatch(function() {
  1440       let charset = null;
  1442       try {
  1443         charset = PlacesUtils.annotations.getPageAnnotation(aURI,
  1444                                                             PlacesUtils.CHARSET_ANNO);
  1445       } catch (ex) { }
  1447       deferred.resolve(charset);
  1448     }, Ci.nsIThread.DISPATCH_NORMAL);
  1450     return deferred.promise;
  1451   },
  1453   /**
  1454    * Promised wrapper for mozIAsyncHistory::updatePlaces for a single place.
  1456    * @param aPlaces
  1457    *        a single mozIPlaceInfo object
  1458    * @resolves {Promise}
  1459    */
  1460   promiseUpdatePlace: function PU_promiseUpdatePlaces(aPlace) {
  1461     let deferred = Promise.defer();
  1462     PlacesUtils.asyncHistory.updatePlaces(aPlace, {
  1463       _placeInfo: null,
  1464       handleResult: function handleResult(aPlaceInfo) {
  1465         this._placeInfo = aPlaceInfo;
  1466       },
  1467       handleError: function handleError(aResultCode, aPlaceInfo) {
  1468         deferred.reject(new Components.Exception("Error", aResultCode));
  1469       },
  1470       handleCompletion: function() {
  1471         deferred.resolve(this._placeInfo);
  1473     });
  1475     return deferred.promise;
  1476   },
  1478   /**
  1479    * Promised wrapper for mozIAsyncHistory::getPlacesInfo for a single place.
  1481    * @param aPlaceIdentifier
  1482    *        either an nsIURI or a GUID (@see getPlacesInfo)
  1483    * @resolves to the place info object handed to handleResult.
  1484    */
  1485   promisePlaceInfo: function PU_promisePlaceInfo(aPlaceIdentifier) {
  1486     let deferred = Promise.defer();
  1487     PlacesUtils.asyncHistory.getPlacesInfo(aPlaceIdentifier, {
  1488       _placeInfo: null,
  1489       handleResult: function handleResult(aPlaceInfo) {
  1490         this._placeInfo = aPlaceInfo;
  1491       },
  1492       handleError: function handleError(aResultCode, aPlaceInfo) {
  1493         deferred.reject(new Components.Exception("Error", aResultCode));
  1494       },
  1495       handleCompletion: function() {
  1496         deferred.resolve(this._placeInfo);
  1498     });
  1500     return deferred.promise;
  1501   },
  1503   /**
  1504    * Gets favicon data for a given page url.
  1506    * @param aPageUrl url of the page to look favicon for.
  1507    * @resolves to an object representing a favicon entry, having the following
  1508    *           properties: { uri, dataLen, data, mimeType }
  1509    * @rejects JavaScript exception if the given url has no associated favicon.
  1510    */
  1511   promiseFaviconData: function (aPageUrl) {
  1512     let deferred = Promise.defer();
  1513     PlacesUtils.favicons.getFaviconDataForPage(NetUtil.newURI(aPageUrl),
  1514       function (aURI, aDataLen, aData, aMimeType) {
  1515         if (aURI) {
  1516           deferred.resolve({ uri: aURI,
  1517                              dataLen: aDataLen,
  1518                              data: aData,
  1519                              mimeType: aMimeType });
  1520         } else {
  1521           deferred.reject();
  1523       });
  1524     return deferred.promise;
  1525   },
  1527   /**
  1528    * Get the unique id for an item (a bookmark, a folder or a separator) given
  1529    * its item id.
  1531    * @param aItemId
  1532    *        an item id
  1533    * @return {Promise}
  1534    * @resolves to the GUID.
  1535    * @rejects if aItemId is invalid.
  1536    */
  1537   promiseItemGUID: function (aItemId) GUIDHelper.getItemGUID(aItemId),
  1539   /**
  1540    * Get the item id for an item (a bookmark, a folder or a separator) given
  1541    * its unique id.
  1543    * @param aGUID
  1544    *        an item GUID
  1545    * @retrun {Promise}
  1546    * @resolves to the GUID.
  1547    * @rejects if there's no item for the given GUID.
  1548    */
  1549   promiseItemId: function (aGUID) GUIDHelper.getItemId(aGUID)
  1550 };
  1552 /**
  1553  * Wraps the provided statement so that invoking cancel() on the pending
  1554  * statement object will always cause a REASON_CANCELED.
  1555  */
  1556 function AsyncStatementCancelWrapper(aStmt) {
  1557   this._stmt = aStmt;
  1559 AsyncStatementCancelWrapper.prototype = {
  1560   _canceled: false,
  1561   _cancel: function() {
  1562     this._canceled = true;
  1563     this._pendingStmt.cancel();
  1564   },
  1565   handleResult: function(aResultSet) {
  1566     this._callback.handleResult(aResultSet);
  1567   },
  1568   handleError: function(aError) {
  1569     Cu.reportError("Async statement execution returned (" + aError.result +
  1570                    "): " + aError.message);
  1571   },
  1572   handleCompletion: function(aReason)
  1574     let reason = this._canceled ?
  1575                    Ci.mozIStorageStatementCallback.REASON_CANCELED :
  1576                    aReason;
  1577     this._callback.handleCompletion(reason);
  1578   },
  1579   executeAsync: function(aCallback) {
  1580     this._pendingStmt = this._stmt.executeAsync(this);
  1581     this._callback = aCallback;
  1582     let self = this;
  1583     return { cancel: function () { self._cancel(); } }
  1587 XPCOMUtils.defineLazyGetter(PlacesUtils, "history", function() {
  1588   return Cc["@mozilla.org/browser/nav-history-service;1"]
  1589            .getService(Ci.nsINavHistoryService)
  1590            .QueryInterface(Ci.nsIBrowserHistory)
  1591            .QueryInterface(Ci.nsPIPlacesDatabase);
  1592 });
  1594 XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "asyncHistory",
  1595                                    "@mozilla.org/browser/history;1",
  1596                                    "mozIAsyncHistory");
  1598 XPCOMUtils.defineLazyGetter(PlacesUtils, "bhistory", function() {
  1599   return PlacesUtils.history;
  1600 });
  1602 XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "favicons",
  1603                                    "@mozilla.org/browser/favicon-service;1",
  1604                                    "mozIAsyncFavicons");
  1606 XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "bookmarks",
  1607                                    "@mozilla.org/browser/nav-bookmarks-service;1",
  1608                                    "nsINavBookmarksService");
  1610 XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "annotations",
  1611                                    "@mozilla.org/browser/annotation-service;1",
  1612                                    "nsIAnnotationService");
  1614 XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "tagging",
  1615                                    "@mozilla.org/browser/tagging-service;1",
  1616                                    "nsITaggingService");
  1618 XPCOMUtils.defineLazyGetter(PlacesUtils, "livemarks", function() {
  1619   return Cc["@mozilla.org/browser/livemark-service;2"].
  1620          getService(Ci.mozIAsyncLivemarks);
  1621 });
  1623 XPCOMUtils.defineLazyGetter(PlacesUtils, "transactionManager", function() {
  1624   let tm = Cc["@mozilla.org/transactionmanager;1"].
  1625            createInstance(Ci.nsITransactionManager);
  1626   tm.AddListener(PlacesUtils);
  1627   this.registerShutdownFunction(function () {
  1628     // Clear all references to local transactions in the transaction manager,
  1629     // this prevents from leaking it.
  1630     this.transactionManager.RemoveListener(this);
  1631     this.transactionManager.clear();
  1632   });
  1634   // Bug 750269
  1635   // The transaction manager keeps strong references to transactions, and by
  1636   // that, also to the global for each transaction.  A transaction, however,
  1637   // could be either the transaction itself (for which the global is this
  1638   // module) or some js-proxy in another global, usually a window.  The later
  1639   // would leak because the transaction lifetime (in the manager's stacks)
  1640   // is independent of the global from which doTransaction was called.
  1641   // To avoid such a leak, we hide the native doTransaction from callers,
  1642   // and let each doTransaction call go through this module.
  1643   // Doing so ensures that, as long as the transaction is any of the
  1644   // PlacesXXXTransaction objects declared in this module, the object
  1645   // referenced by the transaction manager has the module itself as global.
  1646   return Object.create(tm, {
  1647     "doTransaction": {
  1648       value: function(aTransaction) {
  1649         tm.doTransaction(aTransaction);
  1652   });
  1653 });
  1655 XPCOMUtils.defineLazyGetter(this, "bundle", function() {
  1656   const PLACES_STRING_BUNDLE_URI = "chrome://places/locale/places.properties";
  1657   return Cc["@mozilla.org/intl/stringbundle;1"].
  1658          getService(Ci.nsIStringBundleService).
  1659          createBundle(PLACES_STRING_BUNDLE_URI);
  1660 });
  1662 XPCOMUtils.defineLazyServiceGetter(this, "focusManager",
  1663                                    "@mozilla.org/focus-manager;1",
  1664                                    "nsIFocusManager");
  1666 // Sometime soon, likely as part of the transition to mozIAsyncBookmarks,
  1667 // itemIds will be deprecated in favour of GUIDs, which play much better
  1668 // with multiple undo/redo operations.  Because these GUIDs are already stored,
  1669 // and because we don't want to revise the transactions API once more when this
  1670 // happens, transactions are set to work with GUIDs exclusively, in the sense
  1671 // that they may never expose itemIds, nor do they accept them as input.
  1672 // More importantly, transactions which add or remove items guarantee to
  1673 // restore the guids on undo/redo, so that the following transactions that may
  1674 // done or undo can assume the items they're interested in are stil accessible
  1675 // through the same GUID.
  1676 // The current bookmarks API, however, doesn't expose the necessary means for
  1677 // working with GUIDs.  So, until it does, this helper object accesses the
  1678 // Places database directly in order to switch between GUIDs and itemIds, and
  1679 // "restore" GUIDs on items re-created items.
  1680 const REASON_FINISHED = Ci.mozIStorageStatementCallback.REASON_FINISHED;
  1681 let GUIDHelper = {
  1682   // Cache for guid<->itemId paris.
  1683   GUIDsForIds: new Map(),
  1684   idsForGUIDs: new Map(),
  1686   getItemId: function (aGUID) {
  1687     if (this.idsForGUIDs.has(aGUID))
  1688       return Promise.resolve(this.idsForGUIDs.get(aGUID));
  1690     let deferred = Promise.defer();
  1691     let itemId = -1;
  1693     this._getIDStatement.params.guid = aGUID;
  1694     this._getIDStatement.executeAsync({
  1695       handleResult: function (aResultSet) {
  1696         let row = aResultSet.getNextRow();
  1697         if (row)
  1698           itemId = row.getResultByIndex(0);
  1699       },
  1700       handleCompletion: aReason => {
  1701         if (aReason == REASON_FINISHED && itemId != -1) {
  1702           this.ensureObservingRemovedItems();
  1703           this.idsForGUIDs.set(aGUID, itemId);
  1705           deferred.resolve(itemId);
  1707         else if (itemId != -1) {
  1708           deferred.reject("no item found for the given guid");
  1710         else {
  1711           deferred.reject("SQLite Error: " + aReason);
  1714     });
  1716     return deferred.promise;
  1717   },
  1719   getItemGUID: function (aItemId) {
  1720     if (this.GUIDsForIds.has(aItemId))
  1721       return Promise.resolve(this.GUIDsForIds.get(aItemId));
  1723     let deferred = Promise.defer();
  1724     let guid = "";
  1726     this._getGUIDStatement.params.id = aItemId;
  1727     this._getGUIDStatement.executeAsync({
  1728       handleResult: function (aResultSet) {
  1729         let row = aResultSet.getNextRow();
  1730         if (row) {
  1731           guid = row.getResultByIndex(1);
  1733       },
  1734       handleCompletion: aReason => {
  1735         if (aReason == REASON_FINISHED && guid) {
  1736           this.ensureObservingRemovedItems();
  1737           this.GUIDsForIds.set(aItemId, guid);
  1739           deferred.resolve(guid);
  1741         else if (!guid) {
  1742           deferred.reject("no item found for the given itemId");
  1744         else {
  1745           deferred.reject("SQLite Error: " + aReason);
  1748     });
  1750     return deferred.promise;
  1751   },
  1753   ensureObservingRemovedItems: function () {
  1754     if (!("observer" in this)) {
  1755       /**
  1756        * This observers serves two purposes:
  1757        * (1) Invalidate cached id<->guid paris on when items are removed.
  1758        * (2) Cache GUIDs given us free of charge by onItemAdded/onItemRemoved.
  1759       *      So, for exmaple, when the NewBookmark needs the new GUID, we already
  1760       *      have it cached.
  1761       */
  1762       this.observer = {
  1763         onItemAdded: (aItemId, aParentId, aIndex, aItemType, aURI, aTitle,
  1764                       aDateAdded, aGUID, aParentGUID) => {
  1765           this.GUIDsForIds.set(aItemId, aGUID);
  1766           this.GUIDsForIds.set(aParentId, aParentGUID);
  1767         },
  1768         onItemRemoved:
  1769         (aItemId, aParentId, aIndex, aItemTyep, aURI, aGUID, aParentGUID) => {
  1770           this.GUIDsForIds.delete(aItemId);
  1771           this.idsForGUIDs.delete(aGUID);
  1772           this.GUIDsForIds.set(aParentId, aParentGUID);
  1773         },
  1775         QueryInterface: XPCOMUtils.generateQI(Ci.nsINavBookmarkObserver),
  1776         __noSuchMethod__: () => {}, // Catch all all onItem* methods.
  1777       };
  1778       PlacesUtils.bookmarks.addObserver(this.observer, false);
  1779       PlacesUtils.registerShutdownFunction(() => {
  1780         PlacesUtils.bookmarks.removeObserver(this.observer);
  1781       });
  1784 };
  1785 XPCOMUtils.defineLazyGetter(GUIDHelper, "_getIDStatement", () => {
  1786   let s = PlacesUtils.history.DBConnection.createAsyncStatement(
  1787     "SELECT b.id, b.guid from moz_bookmarks b WHERE b.guid = :guid");
  1788   PlacesUtils.registerShutdownFunction( () => s.finalize() );
  1789   return s;
  1790 });
  1791 XPCOMUtils.defineLazyGetter(GUIDHelper, "_getGUIDStatement", () => {
  1792   let s = PlacesUtils.history.DBConnection.createAsyncStatement(
  1793     "SELECT b.id, b.guid from moz_bookmarks b WHERE b.id = :id");
  1794   PlacesUtils.registerShutdownFunction( () => s.finalize() );
  1795   return s;
  1796 });
  1798 ////////////////////////////////////////////////////////////////////////////////
  1799 //// Transactions handlers.
  1801 /**
  1802  * Updates commands in the undo group of the active window commands.
  1803  * Inactive windows commands will be updated on focus.
  1804  */
  1805 function updateCommandsOnActiveWindow()
  1807   let win = focusManager.activeWindow;
  1808   if (win && win instanceof Ci.nsIDOMWindow) {
  1809     // Updating "undo" will cause a group update including "redo".
  1810     win.updateCommands("undo");
  1815 /**
  1816  * Used to cache bookmark information in transactions.
  1818  * @note To avoid leaks any non-primitive property should be copied.
  1819  * @note Used internally, DO NOT EXPORT.
  1820  */
  1821 function TransactionItemCache()
  1825 TransactionItemCache.prototype = {
  1826   set id(v)
  1827     this._id = (parseInt(v) > 0 ? v : null),
  1828   get id()
  1829     this._id || -1,
  1830   set parentId(v)
  1831     this._parentId = (parseInt(v) > 0 ? v : null),
  1832   get parentId()
  1833     this._parentId || -1,
  1834   keyword: null,
  1835   title: null,
  1836   dateAdded: null,
  1837   lastModified: null,
  1838   postData: null,
  1839   itemType: null,
  1840   set uri(v)
  1841     this._uri = (v instanceof Ci.nsIURI ? v.clone() : null),
  1842   get uri()
  1843     this._uri || null,
  1844   set feedURI(v)
  1845     this._feedURI = (v instanceof Ci.nsIURI ? v.clone() : null),
  1846   get feedURI()
  1847     this._feedURI || null,
  1848   set siteURI(v)
  1849     this._siteURI = (v instanceof Ci.nsIURI ? v.clone() : null),
  1850   get siteURI()
  1851     this._siteURI || null,
  1852   set index(v)
  1853     this._index = (parseInt(v) >= 0 ? v : null),
  1854   // Index can be 0.
  1855   get index()
  1856     this._index != null ? this._index : PlacesUtils.bookmarks.DEFAULT_INDEX,
  1857   set annotations(v)
  1858     this._annotations = Array.isArray(v) ? Cu.cloneInto(v, {}) : null,
  1859   get annotations()
  1860     this._annotations || null,
  1861   set tags(v)
  1862     this._tags = (v && Array.isArray(v) ? Array.slice(v) : null),
  1863   get tags()
  1864     this._tags || null,
  1865 };
  1868 /**
  1869  * Base transaction implementation.
  1871  * @note used internally, DO NOT EXPORT.
  1872  */
  1873 function BaseTransaction()
  1877 BaseTransaction.prototype = {
  1878   name: null,
  1879   set childTransactions(v)
  1880     this._childTransactions = (Array.isArray(v) ? Array.slice(v) : null),
  1881   get childTransactions()
  1882     this._childTransactions || null,
  1883   doTransaction: function BTXN_doTransaction() {},
  1884   redoTransaction: function BTXN_redoTransaction() this.doTransaction(),
  1885   undoTransaction: function BTXN_undoTransaction() {},
  1886   merge: function BTXN_merge() false,
  1887   get isTransient() false,
  1888   QueryInterface: XPCOMUtils.generateQI([
  1889     Ci.nsITransaction
  1890   ]),
  1891 };
  1894 /**
  1895  * Transaction for performing several Places Transactions in a single batch. 
  1897  * @param aName
  1898  *        title of the aggregate transactions
  1899  * @param aTransactions
  1900  *        an array of transactions to perform
  1902  * @return nsITransaction object
  1903  */
  1904 this.PlacesAggregatedTransaction =
  1905  function PlacesAggregatedTransaction(aName, aTransactions)
  1907   // Copy the transactions array to decouple it from its prototype, which
  1908   // otherwise keeps alive its associated global object.
  1909   this.childTransactions = aTransactions;
  1910   this.name = aName;
  1911   this.item = new TransactionItemCache();
  1913   // Check child transactions number.  We will batch if we have more than
  1914   // MIN_TRANSACTIONS_FOR_BATCH total number of transactions.
  1915   let countTransactions = function(aTransactions, aTxnCount)
  1917     for (let i = 0;
  1918          i < aTransactions.length && aTxnCount < MIN_TRANSACTIONS_FOR_BATCH;
  1919          ++i, ++aTxnCount) {
  1920       let txn = aTransactions[i];
  1921       if (txn.childTransactions && txn.childTransactions.length > 0)
  1922         aTxnCount = countTransactions(txn.childTransactions, aTxnCount);
  1924     return aTxnCount;
  1927   let txnCount = countTransactions(this.childTransactions, 0);
  1928   this._useBatch = txnCount >= MIN_TRANSACTIONS_FOR_BATCH;
  1931 PlacesAggregatedTransaction.prototype = {
  1932   __proto__: BaseTransaction.prototype,
  1934   doTransaction: function ATXN_doTransaction()
  1936     this._isUndo = false;
  1937     if (this._useBatch)
  1938       PlacesUtils.bookmarks.runInBatchMode(this, null);
  1939     else
  1940       this.runBatched(false);
  1941   },
  1943   undoTransaction: function ATXN_undoTransaction()
  1945     this._isUndo = true;
  1946     if (this._useBatch)
  1947       PlacesUtils.bookmarks.runInBatchMode(this, null);
  1948     else
  1949       this.runBatched(true);
  1950   },
  1952   runBatched: function ATXN_runBatched()
  1954     // Use a copy of the transactions array, so we won't reverse the original
  1955     // one on undoing.
  1956     let transactions = this.childTransactions.slice(0);
  1957     if (this._isUndo)
  1958       transactions.reverse();
  1959     for (let i = 0; i < transactions.length; ++i) {
  1960       let txn = transactions[i];
  1961       if (this.item.parentId != -1)
  1962         txn.item.parentId = this.item.parentId;
  1963       if (this._isUndo)
  1964         txn.undoTransaction();
  1965       else
  1966         txn.doTransaction();
  1969 };
  1972 /**
  1973  * Transaction for creating a new folder.
  1975  * @param aTitle
  1976  *        the title for the new folder
  1977  * @param aParentId
  1978  *        the id of the parent folder in which the new folder should be added
  1979  * @param [optional] aIndex
  1980  *        the index of the item in aParentId
  1981  * @param [optional] aAnnotations
  1982  *        array of annotations to set for the new folder
  1983  * @param [optional] aChildTransactions
  1984  *        array of transactions for items to be created in the new folder
  1986  * @return nsITransaction object
  1987  */
  1988 this.PlacesCreateFolderTransaction =
  1989  function PlacesCreateFolderTransaction(aTitle, aParentId, aIndex, aAnnotations,
  1990                                         aChildTransactions)
  1992   this.item = new TransactionItemCache();
  1993   this.item.title = aTitle;
  1994   this.item.parentId = aParentId;
  1995   this.item.index = aIndex;
  1996   this.item.annotations = aAnnotations;
  1997   this.childTransactions = aChildTransactions;
  2000 PlacesCreateFolderTransaction.prototype = {
  2001   __proto__: BaseTransaction.prototype,
  2003   doTransaction: function CFTXN_doTransaction()
  2005     this.item.id = PlacesUtils.bookmarks.createFolder(this.item.parentId,
  2006                                                       this.item.title,
  2007                                                       this.item.index);
  2008     if (this.item.annotations && this.item.annotations.length > 0)
  2009       PlacesUtils.setAnnotationsForItem(this.item.id, this.item.annotations);
  2011     if (this.childTransactions && this.childTransactions.length > 0) {
  2012       // Set the new parent id into child transactions.
  2013       for (let i = 0; i < this.childTransactions.length; ++i) {
  2014         this.childTransactions[i].item.parentId = this.item.id;
  2017       let txn = new PlacesAggregatedTransaction("Create folder childTxn",
  2018                                                 this.childTransactions);
  2019       txn.doTransaction();
  2021   },
  2023   undoTransaction: function CFTXN_undoTransaction()
  2025     if (this.childTransactions && this.childTransactions.length > 0) {
  2026       let txn = new PlacesAggregatedTransaction("Create folder childTxn",
  2027                                                 this.childTransactions);
  2028       txn.undoTransaction();
  2031     // Remove item only after all child transactions have been reverted.
  2032     PlacesUtils.bookmarks.removeItem(this.item.id);
  2034 };
  2037 /**
  2038  * Transaction for creating a new bookmark.
  2040  * @param aURI
  2041  *        the nsIURI of the new bookmark
  2042  * @param aParentId
  2043  *        the id of the folder in which the bookmark should be added.
  2044  * @param [optional] aIndex
  2045  *        the index of the item in aParentId
  2046  * @param [optional] aTitle
  2047  *        the title of the new bookmark
  2048  * @param [optional] aKeyword
  2049  *        the keyword for the new bookmark
  2050  * @param [optional] aAnnotations
  2051  *        array of annotations to set for the new bookmark
  2052  * @param [optional] aChildTransactions
  2053  *        child transactions to commit after creating the bookmark. Prefer
  2054  *        using any of the arguments above if possible. In general, a child
  2055  *        transations should be used only if the change it does has to be
  2056  *        reverted manually when removing the bookmark item.
  2057  *        a child transaction must support setting its bookmark-item
  2058  *        identifier via an "id" js setter.
  2060  * @return nsITransaction object
  2061  */
  2062 this.PlacesCreateBookmarkTransaction =
  2063  function PlacesCreateBookmarkTransaction(aURI, aParentId, aIndex, aTitle,
  2064                                           aKeyword, aAnnotations,
  2065                                           aChildTransactions)
  2067   this.item = new TransactionItemCache();
  2068   this.item.uri = aURI;
  2069   this.item.parentId = aParentId;
  2070   this.item.index = aIndex;
  2071   this.item.title = aTitle;
  2072   this.item.keyword = aKeyword;
  2073   this.item.annotations = aAnnotations;
  2074   this.childTransactions = aChildTransactions;
  2077 PlacesCreateBookmarkTransaction.prototype = {
  2078   __proto__: BaseTransaction.prototype,
  2080   doTransaction: function CITXN_doTransaction()
  2082     this.item.id = PlacesUtils.bookmarks.insertBookmark(this.item.parentId,
  2083                                                         this.item.uri,
  2084                                                         this.item.index,
  2085                                                         this.item.title);
  2086     if (this.item.keyword) {
  2087       PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id,
  2088                                                   this.item.keyword);
  2090     if (this.item.annotations && this.item.annotations.length > 0)
  2091       PlacesUtils.setAnnotationsForItem(this.item.id, this.item.annotations);
  2093     if (this.childTransactions && this.childTransactions.length > 0) {
  2094       // Set the new item id into child transactions.
  2095       for (let i = 0; i < this.childTransactions.length; ++i) {
  2096         this.childTransactions[i].item.id = this.item.id;
  2098       let txn = new PlacesAggregatedTransaction("Create item childTxn",
  2099                                                 this.childTransactions);
  2100       txn.doTransaction();
  2102   },
  2104   undoTransaction: function CITXN_undoTransaction()
  2106     if (this.childTransactions && this.childTransactions.length > 0) {
  2107       // Undo transactions should always be done in reverse order.
  2108       let txn = new PlacesAggregatedTransaction("Create item childTxn",
  2109                                                 this.childTransactions);
  2110       txn.undoTransaction();
  2113     // Remove item only after all child transactions have been reverted.
  2114     PlacesUtils.bookmarks.removeItem(this.item.id);
  2116 };
  2119 /**
  2120  * Transaction for creating a new separator.
  2122  * @param aParentId
  2123  *        the id of the folder in which the separator should be added
  2124  * @param [optional] aIndex
  2125  *        the index of the item in aParentId
  2127  * @return nsITransaction object
  2128  */
  2129 this.PlacesCreateSeparatorTransaction =
  2130  function PlacesCreateSeparatorTransaction(aParentId, aIndex)
  2132   this.item = new TransactionItemCache();
  2133   this.item.parentId = aParentId;
  2134   this.item.index = aIndex;
  2137 PlacesCreateSeparatorTransaction.prototype = {
  2138   __proto__: BaseTransaction.prototype,
  2140   doTransaction: function CSTXN_doTransaction()
  2142     this.item.id =
  2143       PlacesUtils.bookmarks.insertSeparator(this.item.parentId, this.item.index);
  2144   },
  2146   undoTransaction: function CSTXN_undoTransaction()
  2148     PlacesUtils.bookmarks.removeItem(this.item.id);
  2150 };
  2153 /**
  2154  * Transaction for creating a new livemark item.
  2156  * @see mozIAsyncLivemarks for documentation regarding the arguments.
  2158  * @param aFeedURI
  2159  *        nsIURI of the feed
  2160  * @param [optional] aSiteURI
  2161  *        nsIURI of the page serving the feed
  2162  * @param aTitle
  2163  *        title for the livemark
  2164  * @param aParentId
  2165  *        the id of the folder in which the livemark should be added
  2166  * @param [optional]  aIndex
  2167  *        the index of the livemark in aParentId
  2168  * @param [optional] aAnnotations
  2169  *        array of annotations to set for the new livemark.
  2171  * @return nsITransaction object
  2172  */
  2173 this.PlacesCreateLivemarkTransaction =
  2174  function PlacesCreateLivemarkTransaction(aFeedURI, aSiteURI, aTitle, aParentId,
  2175                                           aIndex, aAnnotations)
  2177   this.item = new TransactionItemCache();
  2178   this.item.feedURI = aFeedURI;
  2179   this.item.siteURI = aSiteURI;
  2180   this.item.title = aTitle;
  2181   this.item.parentId = aParentId;
  2182   this.item.index = aIndex;
  2183   this.item.annotations = aAnnotations;
  2186 PlacesCreateLivemarkTransaction.prototype = {
  2187   __proto__: BaseTransaction.prototype,
  2189   doTransaction: function CLTXN_doTransaction()
  2191     PlacesUtils.livemarks.addLivemark(
  2192       { title: this.item.title
  2193       , feedURI: this.item.feedURI
  2194       , parentId: this.item.parentId
  2195       , index: this.item.index
  2196       , siteURI: this.item.siteURI
  2197       }).then(aLivemark => {
  2198         this.item.id = aLivemark.id;
  2199         if (this.item.annotations && this.item.annotations.length > 0) {
  2200           PlacesUtils.setAnnotationsForItem(this.item.id,
  2201                                             this.item.annotations);
  2203       }, Cu.reportError);
  2204   },
  2206   undoTransaction: function CLTXN_undoTransaction()
  2208     // The getLivemark callback may fail, but it is used just to serialize,
  2209     // so it doesn't matter.
  2210     PlacesUtils.livemarks.getLivemark({ id: this.item.id })
  2211       .then(null, null).then( () => {
  2212         PlacesUtils.bookmarks.removeItem(this.item.id);
  2213       });
  2215 };
  2218 /**
  2219  * Transaction for removing a livemark item.
  2221  * @param aLivemarkId
  2222  *        the identifier of the folder for the livemark.
  2224  * @return nsITransaction object
  2225  * @note used internally by PlacesRemoveItemTransaction, DO NOT EXPORT.
  2226  */
  2227 function PlacesRemoveLivemarkTransaction(aLivemarkId)
  2229   this.item = new TransactionItemCache();
  2230   this.item.id = aLivemarkId;
  2231   this.item.title = PlacesUtils.bookmarks.getItemTitle(this.item.id);
  2232   this.item.parentId = PlacesUtils.bookmarks.getFolderIdForItem(this.item.id);
  2234   let annos = PlacesUtils.getAnnotationsForItem(this.item.id);
  2235   // Exclude livemark service annotations, those will be recreated automatically
  2236   let annosToExclude = [PlacesUtils.LMANNO_FEEDURI,
  2237                         PlacesUtils.LMANNO_SITEURI];
  2238   this.item.annotations = annos.filter(function(aValue, aIndex, aArray) {
  2239       return annosToExclude.indexOf(aValue.name) == -1;
  2240     });
  2241   this.item.dateAdded = PlacesUtils.bookmarks.getItemDateAdded(this.item.id);
  2242   this.item.lastModified =
  2243     PlacesUtils.bookmarks.getItemLastModified(this.item.id);
  2246 PlacesRemoveLivemarkTransaction.prototype = {
  2247   __proto__: BaseTransaction.prototype,
  2249   doTransaction: function RLTXN_doTransaction()
  2251     PlacesUtils.livemarks.getLivemark({ id: this.item.id })
  2252       .then(aLivemark => {
  2253         this.item.feedURI = aLivemark.feedURI;
  2254         this.item.siteURI = aLivemark.siteURI;
  2255         PlacesUtils.bookmarks.removeItem(this.item.id);
  2256       }, Cu.reportError);
  2257   },
  2259   undoTransaction: function RLTXN_undoTransaction()
  2261     // Undo work must be serialized, otherwise won't be able to know the
  2262     // feedURI and siteURI of the livemark.
  2263     // The getLivemark callback is expected to receive a failure status but it
  2264     // is used just to serialize, so doesn't matter.
  2265     PlacesUtils.livemarks.getLivemark({ id: this.item.id })
  2266       .then(null, () => {
  2267         PlacesUtils.livemarks.addLivemark({ parentId: this.item.parentId
  2268                                           , title: this.item.title
  2269                                           , siteURI: this.item.siteURI
  2270                                           , feedURI: this.item.feedURI
  2271                                           , index: this.item.index
  2272                                           , lastModified: this.item.lastModified
  2273                                           }).then(
  2274           aLivemark => {
  2275             let itemId = aLivemark.id;
  2276             PlacesUtils.bookmarks.setItemDateAdded(itemId, this.item.dateAdded);
  2277             PlacesUtils.setAnnotationsForItem(itemId, this.item.annotations);
  2278           }, Cu.reportError);
  2279       });
  2281 };
  2284 /**
  2285  * Transaction for moving an Item.
  2287  * @param aItemId
  2288  *        the id of the item to move
  2289  * @param aNewParentId
  2290  *        id of the new parent to move to
  2291  * @param aNewIndex
  2292  *        index of the new position to move to
  2294  * @return nsITransaction object
  2295  */
  2296 this.PlacesMoveItemTransaction =
  2297  function PlacesMoveItemTransaction(aItemId, aNewParentId, aNewIndex)
  2299   this.item = new TransactionItemCache();
  2300   this.item.id = aItemId;
  2301   this.item.parentId = PlacesUtils.bookmarks.getFolderIdForItem(this.item.id);
  2302   this.new = new TransactionItemCache();
  2303   this.new.parentId = aNewParentId;
  2304   this.new.index = aNewIndex;
  2307 PlacesMoveItemTransaction.prototype = {
  2308   __proto__: BaseTransaction.prototype,
  2310   doTransaction: function MITXN_doTransaction()
  2312     this.item.index = PlacesUtils.bookmarks.getItemIndex(this.item.id);
  2313     PlacesUtils.bookmarks.moveItem(this.item.id,
  2314                                    this.new.parentId, this.new.index);
  2315     this._undoIndex = PlacesUtils.bookmarks.getItemIndex(this.item.id);
  2316   },
  2318   undoTransaction: function MITXN_undoTransaction()
  2320     // moving down in the same parent takes in count removal of the item
  2321     // so to revert positions we must move to oldIndex + 1
  2322     if (this.new.parentId == this.item.parentId &&
  2323         this.item.index > this._undoIndex) {
  2324       PlacesUtils.bookmarks.moveItem(this.item.id, this.item.parentId,
  2325                                      this.item.index + 1);
  2327     else {
  2328       PlacesUtils.bookmarks.moveItem(this.item.id, this.item.parentId,
  2329                                      this.item.index);
  2332 };
  2335 /**
  2336  * Transaction for removing an Item
  2338  * @param aItemId
  2339  *        id of the item to remove
  2341  * @return nsITransaction object
  2342  */
  2343 this.PlacesRemoveItemTransaction =
  2344  function PlacesRemoveItemTransaction(aItemId)
  2346   if (PlacesUtils.isRootItem(aItemId))
  2347     throw Cr.NS_ERROR_INVALID_ARG;
  2349   // if the item lives within a tag container, use the tagging transactions
  2350   let parent = PlacesUtils.bookmarks.getFolderIdForItem(aItemId);
  2351   let grandparent = PlacesUtils.bookmarks.getFolderIdForItem(parent);
  2352   if (grandparent == PlacesUtils.tagsFolderId) {
  2353     let uri = PlacesUtils.bookmarks.getBookmarkURI(aItemId);
  2354     return new PlacesUntagURITransaction(uri, [parent]);
  2357   // if the item is a livemark container we will not save its children.
  2358   if (PlacesUtils.annotations.itemHasAnnotation(aItemId,
  2359                                                 PlacesUtils.LMANNO_FEEDURI))
  2360     return new PlacesRemoveLivemarkTransaction(aItemId);
  2362   this.item = new TransactionItemCache();
  2363   this.item.id = aItemId;
  2364   this.item.itemType = PlacesUtils.bookmarks.getItemType(this.item.id);
  2365   if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_FOLDER) {
  2366     this.childTransactions = this._getFolderContentsTransactions();
  2367     // Remove this folder itself.
  2368     let txn = PlacesUtils.bookmarks.getRemoveFolderTransaction(this.item.id);
  2369     this.childTransactions.push(txn);
  2371   else if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
  2372     this.item.uri = PlacesUtils.bookmarks.getBookmarkURI(this.item.id);
  2373     this.item.keyword =
  2374       PlacesUtils.bookmarks.getKeywordForBookmark(this.item.id);
  2377   if (this.item.itemType != Ci.nsINavBookmarksService.TYPE_SEPARATOR)
  2378     this.item.title = PlacesUtils.bookmarks.getItemTitle(this.item.id);
  2380   this.item.parentId = PlacesUtils.bookmarks.getFolderIdForItem(this.item.id);
  2381   this.item.annotations = PlacesUtils.getAnnotationsForItem(this.item.id);
  2382   this.item.dateAdded = PlacesUtils.bookmarks.getItemDateAdded(this.item.id);
  2383   this.item.lastModified =
  2384     PlacesUtils.bookmarks.getItemLastModified(this.item.id);
  2387 PlacesRemoveItemTransaction.prototype = {
  2388   __proto__: BaseTransaction.prototype,
  2390   doTransaction: function RITXN_doTransaction()
  2392     this.item.index = PlacesUtils.bookmarks.getItemIndex(this.item.id);
  2394     if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_FOLDER) {
  2395       let txn = new PlacesAggregatedTransaction("Remove item childTxn",
  2396                                                 this.childTransactions);
  2397       txn.doTransaction();
  2399     else {
  2400       // Before removing the bookmark, save its tags.
  2401       let tags = this.item.uri ?
  2402         PlacesUtils.tagging.getTagsForURI(this.item.uri) : null;
  2404       PlacesUtils.bookmarks.removeItem(this.item.id);
  2406       // If this was the last bookmark (excluding tag-items) for this url,
  2407       // persist the tags.
  2408       if (tags && PlacesUtils.getMostRecentBookmarkForURI(this.item.uri) == -1) {
  2409         this.item.tags = tags;
  2412   },
  2414   undoTransaction: function RITXN_undoTransaction()
  2416     if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
  2417       this.item.id = PlacesUtils.bookmarks.insertBookmark(this.item.parentId,
  2418                                                           this.item.uri,
  2419                                                           this.item.index,
  2420                                                           this.item.title);
  2421       if (this.item.tags && this.item.tags.length > 0)
  2422         PlacesUtils.tagging.tagURI(this.item.uri, this.item.tags);
  2423       if (this.item.keyword) {
  2424         PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id,
  2425                                                     this.item.keyword);
  2428     else if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_FOLDER) {
  2429       let txn = new PlacesAggregatedTransaction("Remove item childTxn",
  2430                                                 this.childTransactions);
  2431       txn.undoTransaction();
  2433     else { // TYPE_SEPARATOR
  2434       this.item.id = PlacesUtils.bookmarks.insertSeparator(this.item.parentId,
  2435                                                             this.item.index);
  2438     if (this.item.annotations && this.item.annotations.length > 0)
  2439       PlacesUtils.setAnnotationsForItem(this.item.id, this.item.annotations);
  2441     PlacesUtils.bookmarks.setItemDateAdded(this.item.id, this.item.dateAdded);
  2442     PlacesUtils.bookmarks.setItemLastModified(this.item.id,
  2443                                               this.item.lastModified);
  2444   },
  2446   /**
  2447   * Returns a flat, ordered list of transactions for a depth-first recreation
  2448   * of items within this folder.
  2449   */
  2450   _getFolderContentsTransactions:
  2451   function RITXN__getFolderContentsTransactions()
  2453     let transactions = [];
  2454     let contents =
  2455       PlacesUtils.getFolderContents(this.item.id, false, false).root;
  2456     for (let i = 0; i < contents.childCount; ++i) {
  2457       let txn = new PlacesRemoveItemTransaction(contents.getChild(i).itemId);
  2458       transactions.push(txn);
  2460     contents.containerOpen = false;
  2461     // Reverse transactions to preserve parent-child relationship.
  2462     return transactions.reverse();
  2464 };
  2467 /**
  2468  * Transaction for editting a bookmark's title.
  2470  * @param aItemId
  2471  *        id of the item to edit
  2472  * @param aNewTitle
  2473  *        new title for the item to edit
  2475  * @return nsITransaction object
  2476  */
  2477 this.PlacesEditItemTitleTransaction =
  2478  function PlacesEditItemTitleTransaction(aItemId, aNewTitle)
  2480   this.item = new TransactionItemCache();
  2481   this.item.id = aItemId;
  2482   this.new = new TransactionItemCache();
  2483   this.new.title = aNewTitle;
  2486 PlacesEditItemTitleTransaction.prototype = {
  2487   __proto__: BaseTransaction.prototype,
  2489   doTransaction: function EITTXN_doTransaction()
  2491     this.item.title = PlacesUtils.bookmarks.getItemTitle(this.item.id);
  2492     PlacesUtils.bookmarks.setItemTitle(this.item.id, this.new.title);
  2493   },
  2495   undoTransaction: function EITTXN_undoTransaction()
  2497     PlacesUtils.bookmarks.setItemTitle(this.item.id, this.item.title);
  2499 };
  2502 /**
  2503  * Transaction for editing a bookmark's uri.
  2505  * @param aItemId
  2506  *        id of the bookmark to edit
  2507  * @param aNewURI
  2508  *        new uri for the bookmark
  2510  * @return nsITransaction object
  2511  */
  2512 this.PlacesEditBookmarkURITransaction =
  2513  function PlacesEditBookmarkURITransaction(aItemId, aNewURI) {
  2514   this.item = new TransactionItemCache();
  2515   this.item.id = aItemId;
  2516   this.new = new TransactionItemCache();
  2517   this.new.uri = aNewURI;
  2520 PlacesEditBookmarkURITransaction.prototype = {
  2521   __proto__: BaseTransaction.prototype,
  2523   doTransaction: function EBUTXN_doTransaction()
  2525     this.item.uri = PlacesUtils.bookmarks.getBookmarkURI(this.item.id);
  2526     PlacesUtils.bookmarks.changeBookmarkURI(this.item.id, this.new.uri);
  2527     // move tags from old URI to new URI
  2528     this.item.tags = PlacesUtils.tagging.getTagsForURI(this.item.uri);
  2529     if (this.item.tags.length != 0) {
  2530       // only untag the old URI if this is the only bookmark
  2531       if (PlacesUtils.getBookmarksForURI(this.item.uri, {}).length == 0)
  2532         PlacesUtils.tagging.untagURI(this.item.uri, this.item.tags);
  2533       PlacesUtils.tagging.tagURI(this.new.uri, this.item.tags);
  2535   },
  2537   undoTransaction: function EBUTXN_undoTransaction()
  2539     PlacesUtils.bookmarks.changeBookmarkURI(this.item.id, this.item.uri);
  2540     // move tags from new URI to old URI 
  2541     if (this.item.tags.length != 0) {
  2542       // only untag the new URI if this is the only bookmark
  2543       if (PlacesUtils.getBookmarksForURI(this.new.uri, {}).length == 0)
  2544         PlacesUtils.tagging.untagURI(this.new.uri, this.item.tags);
  2545       PlacesUtils.tagging.tagURI(this.item.uri, this.item.tags);
  2548 };
  2551 /**
  2552  * Transaction for setting/unsetting an item annotation
  2554  * @param aItemId
  2555  *        id of the item where to set annotation
  2556  * @param aAnnotationObject
  2557  *        Object representing an annotation, containing the following
  2558  *        properties: name, flags, expires, value.
  2559  *        If value is null the annotation will be removed
  2561  * @return nsITransaction object
  2562  */
  2563 this.PlacesSetItemAnnotationTransaction =
  2564  function PlacesSetItemAnnotationTransaction(aItemId, aAnnotationObject)
  2566   this.item = new TransactionItemCache();
  2567   this.item.id = aItemId;
  2568   this.new = new TransactionItemCache();
  2569   this.new.annotations = [aAnnotationObject];
  2572 PlacesSetItemAnnotationTransaction.prototype = {
  2573   __proto__: BaseTransaction.prototype,
  2575   doTransaction: function SIATXN_doTransaction()
  2577     let annoName = this.new.annotations[0].name;
  2578     if (PlacesUtils.annotations.itemHasAnnotation(this.item.id, annoName)) {
  2579       // fill the old anno if it is set
  2580       let flags = {}, expires = {}, type = {};
  2581       PlacesUtils.annotations.getItemAnnotationInfo(this.item.id, annoName, flags,
  2582                                                     expires, type);
  2583       let value = PlacesUtils.annotations.getItemAnnotation(this.item.id,
  2584                                                             annoName);
  2585       this.item.annotations = [{ name: annoName,
  2586                                 type: type.value,
  2587                                 flags: flags.value,
  2588                                 value: value,
  2589                                 expires: expires.value }];
  2591     else {
  2592       // create an empty old anno
  2593       this.item.annotations = [{ name: annoName,
  2594                                 flags: 0,
  2595                                 value: null,
  2596                                 expires: Ci.nsIAnnotationService.EXPIRE_NEVER }];
  2599     PlacesUtils.setAnnotationsForItem(this.item.id, this.new.annotations);
  2600   },
  2602   undoTransaction: function SIATXN_undoTransaction()
  2604     PlacesUtils.setAnnotationsForItem(this.item.id, this.item.annotations);
  2606 };
  2609 /**
  2610  * Transaction for setting/unsetting a page annotation
  2612  * @param aURI
  2613  *        URI of the page where to set annotation
  2614  * @param aAnnotationObject
  2615  *        Object representing an annotation, containing the following
  2616  *        properties: name, flags, expires, value.
  2617  *        If value is null the annotation will be removed
  2619  * @return nsITransaction object
  2620  */
  2621 this.PlacesSetPageAnnotationTransaction =
  2622  function PlacesSetPageAnnotationTransaction(aURI, aAnnotationObject)
  2624   this.item = new TransactionItemCache();
  2625   this.item.uri = aURI;
  2626   this.new = new TransactionItemCache();
  2627   this.new.annotations = [aAnnotationObject];
  2630 PlacesSetPageAnnotationTransaction.prototype = {
  2631   __proto__: BaseTransaction.prototype,
  2633   doTransaction: function SPATXN_doTransaction()
  2635     let annoName = this.new.annotations[0].name;
  2636     if (PlacesUtils.annotations.pageHasAnnotation(this.item.uri, annoName)) {
  2637       // fill the old anno if it is set
  2638       let flags = {}, expires = {}, type = {};
  2639       PlacesUtils.annotations.getPageAnnotationInfo(this.item.uri, annoName, flags,
  2640                                                     expires, type);
  2641       let value = PlacesUtils.annotations.getPageAnnotation(this.item.uri,
  2642                                                             annoName);
  2643       this.item.annotations = [{ name: annoName,
  2644                                 flags: flags.value,
  2645                                 value: value,
  2646                                 expires: expires.value }];
  2648     else {
  2649       // create an empty old anno
  2650       this.item.annotations = [{ name: annoName,
  2651                                 type: Ci.nsIAnnotationService.TYPE_STRING,
  2652                                 flags: 0,
  2653                                 value: null,
  2654                                 expires: Ci.nsIAnnotationService.EXPIRE_NEVER }];
  2657     PlacesUtils.setAnnotationsForURI(this.item.uri, this.new.annotations);
  2658   },
  2660   undoTransaction: function SPATXN_undoTransaction()
  2662     PlacesUtils.setAnnotationsForURI(this.item.uri, this.item.annotations);
  2664 };
  2667 /**
  2668  * Transaction for editing a bookmark's keyword.
  2670  * @param aItemId
  2671  *        id of the bookmark to edit
  2672  * @param aNewKeyword
  2673  *        new keyword for the bookmark
  2675  * @return nsITransaction object
  2676  */
  2677 this.PlacesEditBookmarkKeywordTransaction =
  2678  function PlacesEditBookmarkKeywordTransaction(aItemId, aNewKeyword)
  2680   this.item = new TransactionItemCache();
  2681   this.item.id = aItemId;
  2682   this.new = new TransactionItemCache();
  2683   this.new.keyword = aNewKeyword;
  2686 PlacesEditBookmarkKeywordTransaction.prototype = {
  2687   __proto__: BaseTransaction.prototype,
  2689   doTransaction: function EBKTXN_doTransaction()
  2691     this.item.keyword = PlacesUtils.bookmarks.getKeywordForBookmark(this.item.id);
  2692     PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id, this.new.keyword);
  2693   },
  2695   undoTransaction: function EBKTXN_undoTransaction()
  2697     PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id, this.item.keyword);
  2699 };
  2702 /**
  2703  * Transaction for editing the post data associated with a bookmark.
  2705  * @param aItemId
  2706  *        id of the bookmark to edit
  2707  * @param aPostData
  2708  *        post data
  2710  * @return nsITransaction object
  2711  */
  2712 this.PlacesEditBookmarkPostDataTransaction =
  2713  function PlacesEditBookmarkPostDataTransaction(aItemId, aPostData)
  2715   this.item = new TransactionItemCache();
  2716   this.item.id = aItemId;
  2717   this.new = new TransactionItemCache();
  2718   this.new.postData = aPostData;
  2721 PlacesEditBookmarkPostDataTransaction.prototype = {
  2722   __proto__: BaseTransaction.prototype,
  2724   doTransaction: function EBPDTXN_doTransaction()
  2726     this.item.postData = PlacesUtils.getPostDataForBookmark(this.item.id);
  2727     PlacesUtils.setPostDataForBookmark(this.item.id, this.new.postData);
  2728   },
  2730   undoTransaction: function EBPDTXN_undoTransaction()
  2732     PlacesUtils.setPostDataForBookmark(this.item.id, this.item.postData);
  2734 };
  2737 /**
  2738  * Transaction for editing an item's date added property.
  2740  * @param aItemId
  2741  *        id of the item to edit
  2742  * @param aNewDateAdded
  2743  *        new date added for the item 
  2745  * @return nsITransaction object
  2746  */
  2747 this.PlacesEditItemDateAddedTransaction =
  2748  function PlacesEditItemDateAddedTransaction(aItemId, aNewDateAdded)
  2750   this.item = new TransactionItemCache();
  2751   this.item.id = aItemId;
  2752   this.new = new TransactionItemCache();
  2753   this.new.dateAdded = aNewDateAdded;
  2756 PlacesEditItemDateAddedTransaction.prototype = {
  2757   __proto__: BaseTransaction.prototype,
  2759   doTransaction: function EIDATXN_doTransaction()
  2761     // Child transactions have the id set as parentId.
  2762     if (this.item.id == -1 && this.item.parentId != -1)
  2763       this.item.id = this.item.parentId;
  2764     this.item.dateAdded =
  2765       PlacesUtils.bookmarks.getItemDateAdded(this.item.id);
  2766     PlacesUtils.bookmarks.setItemDateAdded(this.item.id, this.new.dateAdded);
  2767   },
  2769   undoTransaction: function EIDATXN_undoTransaction()
  2771     PlacesUtils.bookmarks.setItemDateAdded(this.item.id, this.item.dateAdded);
  2773 };
  2776 /**
  2777  * Transaction for editing an item's last modified time.
  2779  * @param aItemId
  2780  *        id of the item to edit
  2781  * @param aNewLastModified
  2782  *        new last modified date for the item 
  2784  * @return nsITransaction object
  2785  */
  2786 this.PlacesEditItemLastModifiedTransaction =
  2787  function PlacesEditItemLastModifiedTransaction(aItemId, aNewLastModified)
  2789   this.item = new TransactionItemCache();
  2790   this.item.id = aItemId;
  2791   this.new = new TransactionItemCache();
  2792   this.new.lastModified = aNewLastModified;
  2795 PlacesEditItemLastModifiedTransaction.prototype = {
  2796   __proto__: BaseTransaction.prototype,
  2798   doTransaction:
  2799   function EILMTXN_doTransaction()
  2801     // Child transactions have the id set as parentId.
  2802     if (this.item.id == -1 && this.item.parentId != -1)
  2803       this.item.id = this.item.parentId;
  2804     this.item.lastModified =
  2805       PlacesUtils.bookmarks.getItemLastModified(this.item.id);
  2806     PlacesUtils.bookmarks.setItemLastModified(this.item.id,
  2807                                               this.new.lastModified);
  2808   },
  2810   undoTransaction:
  2811   function EILMTXN_undoTransaction()
  2813     PlacesUtils.bookmarks.setItemLastModified(this.item.id,
  2814                                               this.item.lastModified);
  2816 };
  2819 /**
  2820  * Transaction for sorting a folder by name
  2822  * @param aFolderId
  2823  *        id of the folder to sort
  2825  * @return nsITransaction object
  2826  */
  2827 this.PlacesSortFolderByNameTransaction =
  2828  function PlacesSortFolderByNameTransaction(aFolderId)
  2830   this.item = new TransactionItemCache();  
  2831   this.item.id = aFolderId;
  2834 PlacesSortFolderByNameTransaction.prototype = {
  2835   __proto__: BaseTransaction.prototype,
  2837   doTransaction: function SFBNTXN_doTransaction()
  2839     this._oldOrder = [];
  2841     let contents =
  2842       PlacesUtils.getFolderContents(this.item.id, false, false).root;
  2843     let count = contents.childCount;
  2845     // sort between separators
  2846     let newOrder = []; 
  2847     let preSep = []; // temporary array for sorting each group of items
  2848     let sortingMethod =
  2849       function (a, b) {
  2850         if (PlacesUtils.nodeIsContainer(a) && !PlacesUtils.nodeIsContainer(b))
  2851           return -1;
  2852         if (!PlacesUtils.nodeIsContainer(a) && PlacesUtils.nodeIsContainer(b))
  2853           return 1;
  2854         return a.title.localeCompare(b.title);
  2855       };
  2857     for (let i = 0; i < count; ++i) {
  2858       let item = contents.getChild(i);
  2859       this._oldOrder[item.itemId] = i;
  2860       if (PlacesUtils.nodeIsSeparator(item)) {
  2861         if (preSep.length > 0) {
  2862           preSep.sort(sortingMethod);
  2863           newOrder = newOrder.concat(preSep);
  2864           preSep.splice(0, preSep.length);
  2866         newOrder.push(item);
  2868       else
  2869         preSep.push(item);
  2871     contents.containerOpen = false;
  2873     if (preSep.length > 0) {
  2874       preSep.sort(sortingMethod);
  2875       newOrder = newOrder.concat(preSep);
  2878     // set the nex indexes
  2879     let callback = {
  2880       runBatched: function() {
  2881         for (let i = 0; i < newOrder.length; ++i) {
  2882           PlacesUtils.bookmarks.setItemIndex(newOrder[i].itemId, i);
  2885     };
  2886     PlacesUtils.bookmarks.runInBatchMode(callback, null);
  2887   },
  2889   undoTransaction: function SFBNTXN_undoTransaction()
  2891     let callback = {
  2892       _self: this,
  2893       runBatched: function() {
  2894         for (item in this._self._oldOrder)
  2895           PlacesUtils.bookmarks.setItemIndex(item, this._self._oldOrder[item]);
  2897     };
  2898     PlacesUtils.bookmarks.runInBatchMode(callback, null);
  2900 };
  2903 /**
  2904  * Transaction for tagging a URL with the given set of tags. Current tags set
  2905  * for the URL persist. It's the caller's job to check whether or not aURI
  2906  * was already tagged by any of the tags in aTags, undoing this tags
  2907  * transaction removes them all from aURL!
  2909  * @param aURI
  2910  *        the URL to tag.
  2911  * @param aTags
  2912  *        Array of tags to set for the given URL.
  2913  */
  2914 this.PlacesTagURITransaction =
  2915  function PlacesTagURITransaction(aURI, aTags)
  2917   this.item = new TransactionItemCache();
  2918   this.item.uri = aURI;
  2919   this.item.tags = aTags;
  2922 PlacesTagURITransaction.prototype = {
  2923   __proto__: BaseTransaction.prototype,
  2925   doTransaction: function TUTXN_doTransaction()
  2927     if (PlacesUtils.getMostRecentBookmarkForURI(this.item.uri) == -1) {
  2928       // There is no bookmark for this uri, but we only allow to tag bookmarks.
  2929       // Force an unfiled bookmark first.
  2930       this.item.id =
  2931         PlacesUtils.bookmarks
  2932                    .insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
  2933                                    this.item.uri,
  2934                                    PlacesUtils.bookmarks.DEFAULT_INDEX,
  2935                                    PlacesUtils.history.getPageTitle(this.item.uri));
  2937     PlacesUtils.tagging.tagURI(this.item.uri, this.item.tags);
  2938   },
  2940   undoTransaction: function TUTXN_undoTransaction()
  2942     if (this.item.id != -1) {
  2943       PlacesUtils.bookmarks.removeItem(this.item.id);
  2944       this.item.id = -1;
  2946     PlacesUtils.tagging.untagURI(this.item.uri, this.item.tags);
  2948 };
  2951 /**
  2952  * Transaction for removing tags from a URL. It's the caller's job to check
  2953  * whether or not aURI isn't tagged by any of the tags in aTags, undoing this
  2954  * tags transaction adds them all to aURL!
  2956  * @param aURI
  2957  *        the URL to un-tag.
  2958  * @param aTags
  2959  *        Array of tags to unset. pass null to remove all tags from the given
  2960  *        url.
  2961  */
  2962 this.PlacesUntagURITransaction =
  2963  function PlacesUntagURITransaction(aURI, aTags)
  2965   this.item = new TransactionItemCache();
  2966   this.item.uri = aURI;
  2967   if (aTags) {
  2968     // Within this transaction, we cannot rely on tags given by itemId
  2969     // since the tag containers may be gone after we call untagURI.
  2970     // Thus, we convert each tag given by its itemId to name.
  2971     let tags = [];
  2972     for (let i = 0; i < aTags.length; ++i) {
  2973       if (typeof(aTags[i]) == "number")
  2974         tags.push(PlacesUtils.bookmarks.getItemTitle(aTags[i]));
  2975       else
  2976         tags.push(aTags[i]);
  2978     this.item.tags = tags;
  2982 PlacesUntagURITransaction.prototype = {
  2983   __proto__: BaseTransaction.prototype,
  2985   doTransaction: function UTUTXN_doTransaction()
  2987     // Filter tags existing on the bookmark, otherwise on undo we may try to
  2988     // set nonexistent tags.
  2989     let tags = PlacesUtils.tagging.getTagsForURI(this.item.uri);
  2990     this.item.tags = this.item.tags.filter(function (aTag) {
  2991       return tags.indexOf(aTag) != -1;
  2992     });
  2993     PlacesUtils.tagging.untagURI(this.item.uri, this.item.tags);
  2994   },
  2996   undoTransaction: function UTUTXN_undoTransaction()
  2998     PlacesUtils.tagging.tagURI(this.item.uri, this.item.tags);
  3000 };

mercurial