text: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
  /* This Source Code Form is subject to the terms of the Mozilla Public
   * License, v. 2.0. If a copy of the MPL was not distributed with this
   * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  
  Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  Components.utils.import("resource://gre/modules/ForgetAboutSite.jsm");
  XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                    "resource://gre/modules/NetUtil.jsm");
  XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                    "resource://gre/modules/PrivateBrowsingUtils.jsm");
  
  // XXXmano: we should move most/all of these constants to PlacesUtils
  const ORGANIZER_ROOT_BOOKMARKS = "place:folder=BOOKMARKS_MENU&excludeItems=1&queryType=1";
  
  // No change to the view, preserve current selection
  const RELOAD_ACTION_NOTHING = 0;
  // Inserting items new to the view, select the inserted rows
  const RELOAD_ACTION_INSERT = 1;
  // Removing items from the view, select the first item after the last selected
  const RELOAD_ACTION_REMOVE = 2;
  // Moving items within a view, don't treat the dropped items as additional
  // rows.
  const RELOAD_ACTION_MOVE = 3;
  
  // When removing a bunch of pages we split them in chunks to give some breath
  // to the main-thread.
  const REMOVE_PAGES_CHUNKLEN = 300;
  
  /**
   * Represents an insertion point within a container where we can insert
   * items.
   * @param   aItemId
   *          The identifier of the parent container
   * @param   aIndex
   *          The index within the container where we should insert
   * @param   aOrientation
   *          The orientation of the insertion. NOTE: the adjustments to the
   *          insertion point to accommodate the orientation should be done by
   *          the person who constructs the IP, not the user. The orientation
   *          is provided for informational purposes only!
   * @param   [optional] aIsTag
   *          Indicates if parent container is a tag
   * @param   [optional] aDropNearItemId
   *          When defined we will calculate index based on this itemId
   * @constructor
   */
  function InsertionPoint(aItemId, aIndex, aOrientation, aIsTag,
                          aDropNearItemId) {
    this.itemId = aItemId;
    this._index = aIndex;
    this.orientation = aOrientation;
    this.isTag = aIsTag;
    this.dropNearItemId = aDropNearItemId;
  }
  
  InsertionPoint.prototype = {
    set index(val) {
      return this._index = val;
    },
  
    get index() {
      if (this.dropNearItemId > 0) {
        // If dropNearItemId is set up we must calculate the real index of
        // the item near which we will drop.
        var index = PlacesUtils.bookmarks.getItemIndex(this.dropNearItemId);
        return this.orientation == Ci.nsITreeView.DROP_BEFORE ? index : index + 1;
      }
      return this._index;
    }
  };
  
  /**
   * Places Controller
   */
  
  function PlacesController(aView) {
    this._view = aView;
    XPCOMUtils.defineLazyServiceGetter(this, "clipboard",
                                       "@mozilla.org/widget/clipboard;1",
                                       "nsIClipboard");
    XPCOMUtils.defineLazyGetter(this, "profileName", function() {
      return Services.dirsvc.get("ProfD", Ci.nsIFile).leafName;
    });
  
    this._cachedLivemarkInfoObjects = new Map();
  }
  
  PlacesController.prototype = {
    /**
     * The places view.
     */
    _view: null,
  
    QueryInterface: XPCOMUtils.generateQI([
      Ci.nsIClipboardOwner
    ]),
  
    // nsIClipboardOwner
    LosingOwnership: function(aXferable) {
      this.cutNodes = [];
    },
  
    terminate: function() {
      this._releaseClipboardOwnership();
    },
  
    supportsCommand: function(aCommand) {
      // Non-Places specific commands that we also support
      switch (aCommand) {
      case "cmd_undo":
      case "cmd_redo":
      case "cmd_cut":
      case "cmd_copy":
      case "cmd_paste":
      case "cmd_delete":
      case "cmd_selectAll":
        return true;
      }
  
      // All other Places Commands are prefixed with "placesCmd_" ... this
      // filters out other commands that we do _not_ support (see 329587).
      const CMD_PREFIX = "placesCmd_";
      return (aCommand.substr(0, CMD_PREFIX.length) == CMD_PREFIX);
    },
  
    isCommandEnabled: function(aCommand) {
      switch (aCommand) {
      case "cmd_undo":
        return PlacesUtils.transactionManager.numberOfUndoItems > 0;
      case "cmd_redo":
        return PlacesUtils.transactionManager.numberOfRedoItems > 0;
      case "cmd_cut":
      case "placesCmd_cut":
      case "placesCmd_moveBookmarks":
        for (let node of this._view.selectedNodes) {
          // If selection includes history nodes or tags-as-bookmark, disallow
          // cutting.
          if (node.itemId == -1 ||
              (node.parent && PlacesUtils.nodeIsTagQuery(node.parent))) {
            return false;
          }
        }
        // Otherwise fall through to the cmd_delete check.
      case "cmd_delete":
      case "placesCmd_delete":
      case "placesCmd_deleteDataHost":
        return this._hasRemovableSelection();
      case "cmd_copy":
      case "placesCmd_copy":
        return this._view.hasSelection;
      case "cmd_paste":
      case "placesCmd_paste":
        return this._canInsert(true) && this._isClipboardDataPasteable();
      case "cmd_selectAll":
        if (this._view.selType != "single") {
          let rootNode = this._view.result.root;
          if (rootNode.containerOpen && rootNode.childCount > 0)
            return true;
        }
        return false;
      case "placesCmd_open":
      case "placesCmd_open:window":
      case "placesCmd_open:privatewindow":
      case "placesCmd_open:tab":
        var selectedNode = this._view.selectedNode;
        return selectedNode && PlacesUtils.nodeIsURI(selectedNode);
      case "placesCmd_new:folder":
      case "placesCmd_new:livemark":
        return this._canInsert();
      case "placesCmd_new:bookmark":
        return this._canInsert();
      case "placesCmd_new:separator":
        return this._canInsert() &&
               !PlacesUtils.asQuery(this._view.result.root).queryOptions.excludeItems &&
               this._view.result.sortingMode ==
                   Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
      case "placesCmd_show:info":
        var selectedNode = this._view.selectedNode;
        return selectedNode && PlacesUtils.getConcreteItemId(selectedNode) != -1
      case "placesCmd_reload":
        // Livemark containers
        var selectedNode = this._view.selectedNode;
        return selectedNode && this.hasCachedLivemarkInfo(selectedNode);
      case "placesCmd_sortBy:name":
        var selectedNode = this._view.selectedNode;
        return selectedNode &&
               PlacesUtils.nodeIsFolder(selectedNode) &&
               !PlacesUIUtils.isContentsReadOnly(selectedNode) &&
               this._view.result.sortingMode ==
                   Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
      case "placesCmd_createBookmark":
        var node = this._view.selectedNode;
        return node && PlacesUtils.nodeIsURI(node) && node.itemId == -1;
      case "placesCmd_openParentFolder":
        return true;
      default:
        return false;
      }
    },
  
    doCommand: function(aCommand) {
      switch (aCommand) {
      case "cmd_undo":
        PlacesUtils.transactionManager.undoTransaction();
        break;
      case "cmd_redo":
        PlacesUtils.transactionManager.redoTransaction();
        break;
      case "cmd_cut":
      case "placesCmd_cut":
        this.cut();
        break;
      case "cmd_copy":
      case "placesCmd_copy":
        this.copy();
        break;
      case "cmd_paste":
      case "placesCmd_paste":
        this.paste();
        break;
      case "cmd_delete":
      case "placesCmd_delete":
        this.remove("Remove Selection");
        break;
      case "placesCmd_deleteDataHost":
        var host;
        if (PlacesUtils.nodeIsHost(this._view.selectedNode)) {
          var queries = this._view.selectedNode.getQueries();
          host = queries[0].domain;
        }
        else
          host = NetUtil.newURI(this._view.selectedNode.uri).host;
        ForgetAboutSite.removeDataFromDomain(host)
                       .catch(Components.utils.reportError);
        break;
      case "cmd_selectAll":
        this.selectAll();
        break;
      case "placesCmd_open":
        PlacesUIUtils.openNodeIn(this._view.selectedNode, "current", this._view);
        break;
      case "placesCmd_open:window":
        PlacesUIUtils.openNodeIn(this._view.selectedNode, "window", this._view);
        break;
      case "placesCmd_open:privatewindow":
        PlacesUIUtils.openNodeIn(this._view.selectedNode, "window", this._view, true);
        break;
      case "placesCmd_open:tab":
        PlacesUIUtils.openNodeIn(this._view.selectedNode, "tab", this._view);
        break;
      case "placesCmd_new:folder":
        this.newItem("folder");
        break;
      case "placesCmd_new:bookmark":
        this.newItem("bookmark");
        break;
      case "placesCmd_new:livemark":
        this.newItem("livemark");
        break;
      case "placesCmd_new:separator":
        this.newSeparator();
        break;
      case "placesCmd_show:info":
        this.showBookmarkPropertiesForSelection();
        break;
      case "placesCmd_moveBookmarks":
        this.moveSelectedBookmarks();
        break;
      case "placesCmd_reload":
        this.reloadSelectedLivemark();
        break;
      case "placesCmd_sortBy:name":
        this.sortFolderByName();
        break;
      case "placesCmd_createBookmark":
        let node = this._view.selectedNode;
        PlacesUIUtils.showBookmarkDialog({ action: "add"
                                         , type: "bookmark"
                                         , hiddenRows: [ "description"
                                                       , "keyword"
                                                       , "location"
                                                       , "loadInSidebar" ]
                                         , uri: NetUtil.newURI(node.uri)
                                         , title: node.title
                                         }, window.top);
        break;
      case "placesCmd_openParentFolder":
        this.openParentFolder();
        break;
      }
    },
  
    onEvent: function(eventName) { },
  
  
    /**
     * Determine whether or not the selection can be removed, either by the
     * delete or cut operations based on whether or not any of its contents
     * are non-removable. We don't need to worry about recursion here since it
     * is a policy decision that a removable item not be placed inside a non-
     * removable item.
     * @returns true if all nodes in the selection can be removed,
     *          false otherwise.
     */
    _hasRemovableSelection() {
      var ranges = this._view.removableSelectionRanges;
      if (!ranges.length)
        return false;
  
      var root = this._view.result.root;
  
      for (var j = 0; j < ranges.length; j++) {
        var nodes = ranges[j];
        for (var i = 0; i < nodes.length; ++i) {
          // Disallow removing the view's root node
          if (nodes[i] == root)
            return false;
  
        if (!PlacesUIUtils.canUserRemove(nodes[i]))
            return false;
        }
      }
  
      return true;
    },
  
    /**
     * Determines whether or not nodes can be inserted relative to the selection.
     */
    _canInsert: function(isPaste) {
      var ip = this._view.insertionPoint;
      return ip != null && (isPaste || ip.isTag != true);
    },
  
    /**
     * Looks at the data on the clipboard to see if it is paste-able.
     * Paste-able data is:
     *   - in a format that the view can receive
     * @returns true if: - clipboard data is of a TYPE_X_MOZ_PLACE_* flavor,
                         - clipboard data is of type TEXT_UNICODE and
                           is a valid URI.
     */
    _isClipboardDataPasteable: function() {
      // if the clipboard contains TYPE_X_MOZ_PLACE_* data, it is definitely
      // pasteable, with no need to unwrap all the nodes.
  
      var flavors = PlacesControllerDragHelper.placesFlavors;
      var clipboard = this.clipboard;
      var hasPlacesData =
        clipboard.hasDataMatchingFlavors(flavors, flavors.length,
                                         Ci.nsIClipboard.kGlobalClipboard);
      if (hasPlacesData)
        return this._view.insertionPoint != null;
  
      // if the clipboard doesn't have TYPE_X_MOZ_PLACE_* data, we also allow
      // pasting of valid "text/unicode" and "text/x-moz-url" data
      var xferable = Cc["@mozilla.org/widget/transferable;1"].
                     createInstance(Ci.nsITransferable);
      xferable.init(null);
  
      xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_URL);
      xferable.addDataFlavor(PlacesUtils.TYPE_UNICODE);
      clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
  
      try {
        // getAnyTransferData will throw if no data is available.
        var data = { }, type = { };
        xferable.getAnyTransferData(type, data, { });
        data = data.value.QueryInterface(Ci.nsISupportsString).data;
        if (type.value != PlacesUtils.TYPE_X_MOZ_URL &&
            type.value != PlacesUtils.TYPE_UNICODE)
          return false;
  
        // unwrapNodes() will throw if the data blob is malformed.
        var unwrappedNodes = PlacesUtils.unwrapNodes(data, type.value);
        return this._view.insertionPoint != null;
      }
      catch (e) {
        // getAnyTransferData or unwrapNodes failed
        return false;
      }
    },
  
    /**
     * Gathers information about the selected nodes according to the following
     * rules:
     *    "link"              node is a URI
     *    "bookmark"          node is a bookmark
     *    "livemarkChild"     node is a child of a livemark
     *    "tagChild"          node is a child of a tag
     *    "folder"            node is a folder
     *    "query"             node is a query
     *    "separator"         node is a separator line
     *    "host"              node is a host
     *
     * @returns an array of objects corresponding the selected nodes. Each
     *          object has each of the properties above set if its corresponding
     *          node matches the rule. In addition, the annotations names for each
     *          node are set on its corresponding object as properties.
     * Notes:
     *   1) This can be slow, so don't call it anywhere performance critical!
     */
    _buildSelectionMetadata: function() {
      var metadata = [];
      var nodes = this._view.selectedNodes;
  
      for (var i = 0; i < nodes.length; i++) {
        var nodeData = {};
        var node = nodes[i];
        var nodeType = node.type;
        var uri = null;
  
        // We don't use the nodeIs* methods here to avoid going through the type
        // property way too often
        switch (nodeType) {
          case Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY:
            nodeData["query"] = true;
            if (node.parent) {
              switch (PlacesUtils.asQuery(node.parent).queryOptions.resultType) {
                case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY:
                  nodeData["host"] = true;
                  break;
                case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY:
                case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY:
                  nodeData["day"] = true;
                  break;
              }
            }
            break;
          case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER:
          case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT:
            nodeData["folder"] = true;
            break;
          case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR:
            nodeData["separator"] = true;
            break;
          case Ci.nsINavHistoryResultNode.RESULT_TYPE_URI:
            nodeData["link"] = true;
            uri = NetUtil.newURI(node.uri);
            if (PlacesUtils.nodeIsBookmark(node)) {
              nodeData["bookmark"] = true;
              var parentNode = node.parent;
              if (parentNode) {
                if (PlacesUtils.nodeIsTagQuery(parentNode))
                  nodeData["tagChild"] = true;
              }
            } else {
              var parentNode = node.parent;
              if (parentNode) {
                if (this.hasCachedLivemarkInfo(parentNode))
                  nodeData["livemarkChild"] = true;
              }
            }
            break;
        }
  
        // annotations
        if (uri) {
          let names = PlacesUtils.annotations.getPageAnnotationNames(uri);
          for (let j = 0; j < names.length; ++j)
            nodeData[names[j]] = true;
        }
  
        // For items also include the item-specific annotations
        if (node.itemId != -1) {
          let names = PlacesUtils.annotations
                                 .getItemAnnotationNames(node.itemId);
          for (let j = 0; j < names.length; ++j)
            nodeData[names[j]] = true;
        }
        metadata.push(nodeData);
      }
  
      return metadata;
    },
  
    /**
     * Determines if a context-menu item should be shown
     * @param   aMenuItem
     *          the context menu item
     * @param   aMetaData
     *          meta data about the selection
     * @returns true if the conditions (see buildContextMenu) are satisfied
     *          and the item can be displayed, false otherwise.
     */
    _shouldShowMenuItem: function(aMenuItem, aMetaData) {
      var selectiontype = aMenuItem.getAttribute("selectiontype");
      if (!selectiontype) {
        selectiontype = "single|multiple";
      }
      var selectionTypes = selectiontype.split("|");
      if (selectionTypes.indexOf("any") != -1) {
        return true;
      }
      var count = aMetaData.length;
      if (count > 1 && selectionTypes.indexOf("multiple") == -1)
        return false;
      if (count == 1 && selectionTypes.indexOf("single") == -1)
        return false;
      // NB: if there is no selection, we show the item if (and only if)
      // the selectiontype includes 'none' - the metadata list will be
      // empty so none of the other criteria will apply anyway.
      if (count == 0)
        return selectionTypes.indexOf("none") != -1;
  
      var forceHideAttr = aMenuItem.getAttribute("forcehideselection");
      if (forceHideAttr) {
        var forceHideRules = forceHideAttr.split("|");
        for (let i = 0; i < aMetaData.length; ++i) {
          for (let j = 0; j < forceHideRules.length; ++j) {
            if (forceHideRules[j] in aMetaData[i])
              return false;
          }
        }
      }
  
      var selectionAttr = aMenuItem.getAttribute("selection");
      if (!selectionAttr) {
        return !aMenuItem.hidden;
      }
  
      if (selectionAttr == "any")
        return true;
  
      var showRules = selectionAttr.split("|");
      var anyMatched = false;
      function metaDataNodeMatches(metaDataNode, rules) {
        for (var i = 0; i < rules.length; i++) {
          if (rules[i] in metaDataNode)
            return true;
        }
        return false;
      }
  
      for (var i = 0; i < aMetaData.length; ++i) {
        if (metaDataNodeMatches(aMetaData[i], showRules))
          anyMatched = true;
        else
          return false;
      }
      return anyMatched;
    },
  
    /**
     * Detects information (meta-data rules) about the current selection in the
     * view (see _buildSelectionMetadata) and sets the visibility state for each
     * of the menu-items in the given popup with the following rules applied:
     *  1) The "selectiontype" attribute may be set on a menu-item to "single"
     *     if the menu-item should be visible only if there is a single node
     *     selected, or to "multiple" if the menu-item should be visible only if
     *     multiple nodes are selected, or to "none" if the menuitems should be
     *     visible for if there are no selected nodes, or to a |-separated
     *     combination of these.
     *     If the attribute is not set or set to an invalid value, the menu-item
     *     may be visible irrespective of the selection.
     *  2) The "selection" attribute may be set on a menu-item to the various
     *     meta-data rules for which it may be visible. The rules should be
     *     separated with the | character.
     *  3) A menu-item may be visible only if at least one of the rules set in
     *     its selection attribute apply to each of the selected nodes in the
     *     view.
     *  4) The "forcehideselection" attribute may be set on a menu-item to rules
     *     for which it should be hidden. This attribute takes priority over the
     *     selection attribute. A menu-item would be hidden if at least one of the
     *     given rules apply to one of the selected nodes. The rules should be
     *     separated with the | character.
     *  5) The "hideifnoinsertionpoint" attribute may be set on a menu-item to
     *     true if it should be hidden when there's no insertion point
     *  6) The visibility state of a menu-item is unchanged if none of these
     *     attribute are set.
     *  7) These attributes should not be set on separators for which the
     *     visibility state is "auto-detected."
     *  8) The "hideifprivatebrowsing" attribute may be set on a menu-item to
     *     true if it should be hidden inside the private browsing mode
     * @param   aPopup
     *          The menupopup to build children into.
     * @return true if at least one item is visible, false otherwise.
     */
    buildContextMenu: function(aPopup) {
      var metadata = this._buildSelectionMetadata();
      var ip = this._view.insertionPoint;
      var noIp = !ip || ip.isTag;
  
      var separator = null;
      var visibleItemsBeforeSep = false;
      var usableItemCount = 0;
      for (var i = 0; i < aPopup.childNodes.length; ++i) {
        var item = aPopup.childNodes[i];
        if (item.localName != "menuseparator") {
          // We allow pasting into tag containers, so special case that.
          var hideIfNoIP = item.getAttribute("hideifnoinsertionpoint") == "true" &&
                           noIp && !(ip && ip.isTag && item.id == "placesContext_paste");
          // Show the "Open Containing Folder" menu-item only when the context is
          // in the Library or in the Sidebar, and only when there's no insertion
          // point.
          var hideParentFolderItem = item.id == "placesContext_openParentFolder" &&
                                     (!/tree/i.test(this._view.localName) || ip);
          var hideIfPrivate = item.getAttribute("hideifprivatebrowsing") == "true" &&
                              PrivateBrowsingUtils.isWindowPrivate(window);
          var shouldHideItem = hideIfNoIP || hideIfPrivate || hideParentFolderItem ||
                               !this._shouldShowMenuItem(item, metadata);
          item.hidden = item.disabled = shouldHideItem;
  
          if (!item.hidden) {
            visibleItemsBeforeSep = true;
            usableItemCount++;
  
            // Show the separator above the menu-item if any
            if (separator) {
              separator.hidden = false;
              separator = null;
            }
          }
        }
        else { // menuseparator
          // Initially hide it. It will be unhidden if there will be at least one
          // visible menu-item above and below it.
          item.hidden = true;
  
          // We won't show the separator at all if no items are visible above it
          if (visibleItemsBeforeSep)
            separator = item;
  
          // New separator, count again:
          visibleItemsBeforeSep = false;
        }
      }
  
      // Set Open Folder/Links In Tabs items enabled state if they're visible
      if (usableItemCount > 0) {
        var openContainerInTabsItem = document.getElementById("placesContext_openContainer:tabs");
        if (!openContainerInTabsItem.hidden) {
          var containerToUse = this._view.selectedNode || this._view.result.root;
          if (PlacesUtils.nodeIsContainer(containerToUse)) {
            if (!PlacesUtils.hasChildURIs(containerToUse)) {
              openContainerInTabsItem.disabled = true;
              // Ensure that we don't display the menu if nothing is enabled:
              usableItemCount--;
            }
          }
        }
      }
  
      return usableItemCount > 0;
    },
  
    /**
     * Select all links in the current view.
     */
    selectAll: function() {
      this._view.selectAll();
    },
  
    /**
     * Opens the bookmark properties for the selected URI Node.
     */
    showBookmarkPropertiesForSelection:
    function() {
      var node = this._view.selectedNode;
      if (!node)
        return;
  
      var itemType = PlacesUtils.nodeIsFolder(node) ||
                     PlacesUtils.nodeIsTagQuery(node) ? "folder" : "bookmark";
      var concreteId = PlacesUtils.getConcreteItemId(node);
      var isRootItem = PlacesUtils.isRootItem(concreteId);
      var itemId = node.itemId;
      if (isRootItem || PlacesUtils.nodeIsTagQuery(node)) {
        // If this is a root or the Tags query we use the concrete itemId to catch
        // the correct title for the node.
        itemId = concreteId;
      }
  
      PlacesUIUtils.showBookmarkDialog({ action: "edit"
                                       , type: itemType
                                       , itemId: itemId
                                       , readOnly: isRootItem
                                       , hiddenRows: [ "folderPicker" ]
                                       }, window.top);
    },
  
    /**
     * This method can be run on a URI parameter to ensure that it didn't
     * receive a string instead of an nsIURI object.
     */
    _assertURINotString: function(value) {
      NS_ASSERT((typeof(value) == "object") && !(value instanceof String),
             "This method should be passed a URI as a nsIURI object, not as a string.");
    },
  
    /**
     * Reloads the selected livemark if any.
     */
    reloadSelectedLivemark: function() {
      var selectedNode = this._view.selectedNode;
      if (selectedNode) {
        let itemId = selectedNode.itemId;
        PlacesUtils.livemarks.getLivemark({ id: itemId })
          .then(aLivemark => {
            aLivemark.reload(true);
          }, Components.utils.reportError);
      }
    },
  
    /**
     * Opens the links in the selected folder, or the selected links in new tabs.
     */
    openSelectionInTabs: function(aEvent) {
      var node = this._view.selectedNode;
      var nodes = this._view.selectedNodes;
      // In the case of no selection, open the root node:
      if (!node && !nodes.length) {
        node = this._view.result.root;
      }
      if (node && PlacesUtils.nodeIsContainer(node))
        PlacesUIUtils.openContainerNodeInTabs(node, aEvent, this._view);
      else
        PlacesUIUtils.openURINodesInTabs(nodes, aEvent, this._view);
    },
  
    /**
     * Shows the Add Bookmark UI for the current insertion point.
     *
     * @param aType
     *        the type of the new item (bookmark/livemark/folder)
     */
    newItem: function(aType) {
      let ip = this._view.insertionPoint;
      if (!ip)
        throw Cr.NS_ERROR_NOT_AVAILABLE;
  
      let performed =
        PlacesUIUtils.showBookmarkDialog({ action: "add"
                                         , type: aType
                                         , defaultInsertionPoint: ip
                                         , hiddenRows: [ "folderPicker" ]
                                         }, window.top);
      if (performed) {
        // Select the new item.
        let insertedNodeId = PlacesUtils.bookmarks
                                        .getIdForItemAt(ip.itemId, ip.index);
        this._view.selectItems([insertedNodeId], false);
      }
    },
  
    /**
     * Create a new Bookmark separator somewhere.
     */
    newSeparator: function() {
      var ip = this._view.insertionPoint;
      if (!ip)
        throw Cr.NS_ERROR_NOT_AVAILABLE;
      var txn = new PlacesCreateSeparatorTransaction(ip.itemId, ip.index);
      PlacesUtils.transactionManager.doTransaction(txn);
      // select the new item
      var insertedNodeId = PlacesUtils.bookmarks
                                      .getIdForItemAt(ip.itemId, ip.index);
      this._view.selectItems([insertedNodeId], false);
    },
  
    /**
     * Opens a dialog for moving the selected nodes.
     */
    moveSelectedBookmarks: function() {
      window.openDialog("chrome://browser/content/places/moveBookmarks.xul",
                        "", "chrome, modal",
                        this._view.selectedNodes);
    },
  
    /**
     * Sort the selected folder by name.
     */
    sortFolderByName: function() {
      var itemId = PlacesUtils.getConcreteItemId(this._view.selectedNode);
      var txn = new PlacesSortFolderByNameTransaction(itemId);
      PlacesUtils.transactionManager.doTransaction(txn);
    },
  
    /**
     * Open the parent folder for the selected bookmarks search result.
     */
    openParentFolder: function() {
      var view;
      if (!document.popupNode) {
        view = document.commandDispatcher.focusedElement;
      } else {
        view = PlacesUIUtils.getViewForNode(document.popupNode); // XULElement
      }
      if (!view || view.getAttribute("type") != "places")
        return;
      var node = view.selectedNode; // nsINavHistoryResultNode
      var aItemId = node.itemId;
      var aFolderItemId = this.getParentFolderByItemId(aItemId);
      if (aFolderItemId)
        this.selectFolderByItemId(view, aFolderItemId, aItemId);
    },
  
    getParentFolderByItemId: function(aItemId) {
      var bmsvc = Components.classes["@mozilla.org/browser/nav-bookmarks-service;1"].
                             getService(Components.interfaces.nsINavBookmarksService);
      var parentFolderId = bmsvc.getFolderIdForItem(aItemId);
  
      return parentFolderId;
    },
  
    selectItems2: function(view, aIDs) {
      var ids = aIDs; // Don't manipulate the caller's array.
  
      // Array of nodes found by findNodes which are to be selected
      var nodes = [];
  
      // Array of nodes found by findNodes which should be opened
      var nodesToOpen = [];
  
      // A set of URIs of container-nodes that were previously searched,
      // and thus shouldn't be searched again. This is empty at the initial
      // start of the recursion and gets filled in as the recursion
      // progresses.
      var nodesURIChecked = [];
  
      /**
       * Recursively search through a node's children for items
       * with the given IDs. When a matching item is found, remove its ID
       * from the IDs array, and add the found node to the nodes dictionary.
       *
       * NOTE: This method will leave open any node that had matching items
       * in its subtree.
       */
      function findNodes(node) {
        var foundOne = false;
        // See if node matches an ID we wanted; add to results.
        // For simple folder queries, check both itemId and the concrete
        // item id.
        var index = ids.indexOf(node.itemId);
        if (index == -1 &&
            node.type == Components.interfaces.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT) {
          index = ids.indexOf(PlacesUtils.asQuery(node).folderItemId); //xxx Bug 556739 3.7a5pre
        }
  
        if (index != -1) {
          nodes.push(node);
          foundOne = true;
          ids.splice(index, 1);
        }
  
        if (ids.length == 0 || !PlacesUtils.nodeIsContainer(node) ||
            nodesURIChecked.indexOf(node.uri) != -1)
          return foundOne;
  
        nodesURIChecked.push(node.uri);
        PlacesUtils.asContainer(node); // xxx Bug 556739 3.7a6pre
  
        // Remember the beginning state so that we can re-close
        // this node if we don't find any additional results here.
        var previousOpenness = node.containerOpen;
        node.containerOpen = true;
        for (var child = 0;  child < node.childCount && ids.length > 0;
             child++) {
          var childNode = node.getChild(child);
          var found = findNodes(childNode);
          if (!foundOne)
            foundOne = found;
        }
  
        // If we didn't find any additional matches in this node's
        // subtree, revert the node to its previous openness.
        if (foundOne)
          nodesToOpen.unshift(node);
        node.containerOpen = previousOpenness;
        return foundOne;
      } // findNodes
  
      // Disable notifications while looking for nodes.
      let result = view.result;
      let didSuppressNotifications = result.suppressNotifications;
      if (!didSuppressNotifications)
        result.suppressNotifications = true
      try {
        findNodes(view.result.root);
      }
      finally {
        if (!didSuppressNotifications)
          result.suppressNotifications = false;
      }
  
      // For all the nodes we've found, highlight the corresponding
      // index in the tree.
      var resultview = view.view;
      var selection = resultview.selection;
      selection.selectEventsSuppressed = true;
      selection.clearSelection();
      // Open nodes containing found items.
      for (var i = 0; i < nodesToOpen.length; i++) {
        nodesToOpen[i].containerOpen = true;
      }
      for (var i = 0; i < nodes.length; i++) {
        if (PlacesUtils.nodeIsContainer(nodes[i]))
          continue;
  
        var index = resultview.treeIndexForNode(nodes[i]);
        selection.rangedSelect(index, index, true);
      }
      selection.selectEventsSuppressed = false;
    },
  
    selectFolderByItemId: function(view, aFolderItemId, aItemId) {
      // Library
      if (view.getAttribute("id") == "placeContent") {
        view = document.getElementById("placesList");
        // Select a folder node in folder pane.
        this.selectItems2(view, [aFolderItemId]);
        view.selectItems([aFolderItemId]);
        if (view.currentIndex)
          view.treeBoxObject.ensureRowIsVisible(view.currentIndex);
        // Reselect child node.
        setTimeout(function(aItemId, view) {
          var aView = view.ownerDocument.getElementById("placeContent");
          aView.selectItems([aItemId]);
          if (aView.currentIndex)
            aView.treeBoxObject.ensureRowIsVisible(aView.currentIndex);
        }, 0, aItemId, view);
        return;
      }
  
      // Bookmarks Sidebar
      if (!view)
        return;
      view.place = view.place;
  
      if ('FlatBookmarksOverlay' in window) {
        var sidebarwin = view.ownerDocument.defaultView;
        var searchBox = sidebarwin.document.getElementById("search-box");
        searchBox.value = "";
        searchBox.doCommand();
        sidebarwin.FlatBookmarks._setTreePlace(sidebarwin.FlatBookmarks._makePlaceForFolder(aFolderItemId));
        view.selectItems([aItemId]);
        var tbo = view.treeBoxObject;
        tbo.ensureRowIsVisible(view.currentIndex);
        view.focus();
        return;
      }
  
      view.findNode = function flatChildNodes(node, aIDs) {
        var ids = aIDs; // Don't manipulate the caller's array.
  
        // Array of nodes found by findNodes which are to be selected
        var nodes = [];
  
        // Array of nodes found by findNodes which should be opened
        var nodesToOpen = [];
  
        // A set of URIs of container-nodes that were previously searched,
        // and thus shouldn't be searched again. This is empty at the initial
        // start of the recursion and gets filled in as the recursion
        // progresses.
        var nodesURIChecked = [];
  
        /**
         * Recursively search through a node's children for items
         * with the given IDs. When a matching item is found, remove its ID
         * from the IDs array, and add the found node to the nodes dictionary.
         *
         * NOTE: This method will leave open any node that had matching items
         * in its subtree.
         */
        function findNodes(node) {
          var foundOne = false;
          // See if node matches an ID we wanted; add to results.
          // For simple folder queries, check both itemId and the concrete
          // item id.
          var index = ids.indexOf(node.itemId);
          if (index == -1 &&
              node.type == Components.interfaces.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT) {
            index = ids.indexOf(PlacesUtils.asQuery(node).folderItemId); // xxx Bug 556739 3.7a5pre
          }
  
          if (index != -1) {
            nodes.push(node);
            foundOne = true;
            ids.splice(index, 1);
          }
  
          if (ids.length == 0 || !PlacesUtils.nodeIsContainer(node) ||
              nodesURIChecked.indexOf(node.uri) != -1)
            return foundOne;
  
          nodesURIChecked.push(node.uri);
          PlacesUtils.asContainer(node); // xxx Bug 556739 3.7a6pre
          // Remember the beginning state so that we can re-close
          // this node if we don't find any additional results here.
          var previousOpenness = node.containerOpen;
          node.containerOpen = true;
          for (var child = 0;  child < node.childCount && ids.length > 0;
               child++) {
            var childNode = node.getChild(child);
            if (PlacesUtils.nodeIsQuery(childNode))
              continue;
            var found = findNodes(childNode);
            if (!foundOne)
              foundOne = found;
          }
  
          // If we didn't find any additional matches in this node's
          // subtree, revert the node to its previous openness.
          if (foundOne)
            nodesToOpen.unshift(node);
          node.containerOpen = previousOpenness;
          return foundOne;
        } // findNodes
  
        // Disable notifications while looking for nodes.
        let result = this.result;
        let didSuppressNotifications = result.suppressNotifications;
        if (!didSuppressNotifications)
          result.suppressNotifications = true
        try {
          findNodes(this.result.root);
        }
        finally {
          if (!didSuppressNotifications)
            result.suppressNotifications = false;
        }
  
        // Open nodes containing found items.
        for (var i = 0; i < nodesToOpen.length; i++) {
          nodesToOpen[i].containerOpen = true;
        }
        return nodes;
      }; // findNode
  
      // For all the nodes we've found, highlight the corresponding
      // index in the tree.
      var resultview = view.view;
      var selection = view.view.selection;
      selection.selectEventsSuppressed = true;
      selection.clearSelection();
      var nodes = view.findNode(view.result.root, [aFolderItemId]);
      if (nodes.length > 0) {
        var index = resultview.treeIndexForNode(nodes[0]);
        nodes = view.findNode(nodes[0], [aItemId]);
        if (nodes.length > 0) {
          index = resultview.treeIndexForNode(nodes[0]);
          selection.rangedSelect(index, index, true);
        }
      }
      selection.selectEventsSuppressed = false;
  
      var tbo = view.treeBoxObject;
      tbo.ensureRowIsVisible(view.currentIndex);
      view.focus();
      return;
    },
  
    /**
     * Walk the list of folders we're removing in this delete operation, and
     * see if the selected node specified is already implicitly being removed
     * because it is a child of that folder.
     * @param   node
     *          Node to check for containment.
     * @param   pastFolders
     *          List of folders the calling function has already traversed
     * @returns true if the node should be skipped, false otherwise.
     */
    _shouldSkipNode: function(node, pastFolders) {
      /**
       * Determines if a node is contained by another node within a resultset.
       * @param   node
       *          The node to check for containment for
       * @param   parent
       *          The parent container to check for containment in
       * @returns true if node is a member of parent's children, false otherwise.
       */
      function isContainedBy(node, parent) {
        var cursor = node.parent;
        while (cursor) {
          if (cursor == parent)
            return true;
          cursor = cursor.parent;
        }
        return false;
      }
  
        for (var j = 0; j < pastFolders.length; ++j) {
          if (isContainedBy(node, pastFolders[j]))
            return true;
        }
        return false;
    },
  
    /**
     * Creates a set of transactions for the removal of a range of items.
     * A range is an array of adjacent nodes in a view.
     * @param   [in] range
     *          An array of nodes to remove. Should all be adjacent.
     * @param   [out] transactions
     *          An array of transactions.
     * @param   [optional] removedFolders
     *          An array of folder nodes that have already been removed.
     */
    _removeRange: function(range, transactions, removedFolders) {
      NS_ASSERT(transactions instanceof Array, "Must pass a transactions array");
      if (!removedFolders)
        removedFolders = [];
  
      for (var i = 0; i < range.length; ++i) {
        var node = range[i];
        if (this._shouldSkipNode(node, removedFolders))
          continue;
  
        if (PlacesUtils.nodeIsTagQuery(node.parent)) {
          // This is a uri node inside a tag container.  It needs a special
          // untag transaction.
          var tagItemId = PlacesUtils.getConcreteItemId(node.parent);
          var uri = NetUtil.newURI(node.uri);
          let txn = new PlacesUntagURITransaction(uri, [tagItemId]);
          transactions.push(txn);
        }
        else if (PlacesUtils.nodeIsTagQuery(node) && node.parent &&
                 PlacesUtils.nodeIsQuery(node.parent) &&
                 PlacesUtils.asQuery(node.parent).queryOptions.resultType ==
                   Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY) {
          // This is a tag container.
          // Untag all URIs tagged with this tag only if the tag container is
          // child of the "Tags" query in the library, in all other places we
          // must only remove the query node.
          var tag = node.title;
          var URIs = PlacesUtils.tagging.getURIsForTag(tag);
          for (var j = 0; j < URIs.length; j++) {
            let txn = new PlacesUntagURITransaction(URIs[j], [tag]);
            transactions.push(txn);
          }
        }
        else if (PlacesUtils.nodeIsURI(node) &&
                 PlacesUtils.nodeIsQuery(node.parent) &&
                 PlacesUtils.asQuery(node.parent).queryOptions.queryType ==
                   Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
          // This is a uri node inside an history query.
          PlacesUtils.bhistory.removePage(NetUtil.newURI(node.uri));
          // History deletes are not undoable, so we don't have a transaction.
        }
        else if (node.itemId == -1 &&
                 PlacesUtils.nodeIsQuery(node) &&
                 PlacesUtils.asQuery(node).queryOptions.queryType ==
                   Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
          // This is a dynamically generated history query, like queries
          // grouped by site, time or both.  Dynamically generated queries don't
          // have an itemId even if they are descendants of a bookmark.
          this._removeHistoryContainer(node);
          // History deletes are not undoable, so we don't have a transaction.
        }
        else {
          // This is a common bookmark item.
          if (PlacesUtils.nodeIsFolder(node)) {
            // If this is a folder we add it to our array of folders, used
            // to skip nodes that are children of an already removed folder.
            removedFolders.push(node);
          }
          let txn = new PlacesRemoveItemTransaction(node.itemId);
          transactions.push(txn);
        }
      }
    },
  
    /**
     * Removes the set of selected ranges from bookmarks.
     * @param   txnName
     *          See |remove|.
     */
    _removeRowsFromBookmarks: function(txnName) {
      var ranges = this._view.removableSelectionRanges;
      var transactions = [];
      var removedFolders = [];
  
      for (var i = 0; i < ranges.length; i++)
        this._removeRange(ranges[i], transactions, removedFolders);
  
      if (transactions.length > 0) {
        var txn = new PlacesAggregatedTransaction(txnName, transactions);
        PlacesUtils.transactionManager.doTransaction(txn);
      }
    },
  
    /**
     * Removes the set of selected ranges from history.
     *
     * @note history deletes are not undoable.
     */
    _removeRowsFromHistory: function() {
      let nodes = this._view.selectedNodes;
      let URIs = [];
      for (let i = 0; i < nodes.length; ++i) {
        let node = nodes[i];
        if (PlacesUtils.nodeIsURI(node)) {
          let uri = NetUtil.newURI(node.uri);
          // Avoid duplicates.
          if (URIs.indexOf(uri) < 0) {
            URIs.push(uri);
          }
        }
        else if (PlacesUtils.nodeIsQuery(node) &&
                 PlacesUtils.asQuery(node).queryOptions.queryType ==
                   Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
          this._removeHistoryContainer(node);
        }
      }
  
      // Do removal in chunks to give some breath to main-thread.
      function pagesChunkGenerator(aURIs) {
        while (aURIs.length) {
          let URIslice = aURIs.splice(0, REMOVE_PAGES_CHUNKLEN);
          PlacesUtils.bhistory.removePages(URIslice, URIslice.length);
          Services.tm.mainThread.dispatch(function() {
            try {
              gen.next();
            } catch (ex if ex instanceof StopIteration) {}
          }, Ci.nsIThread.DISPATCH_NORMAL);
          yield;
        }
      }
      let gen = pagesChunkGenerator(URIs);
      gen.next();
    },
  
    /**
     * Removes history visits for an history container node.
     * @param   [in] aContainerNode
     *          The container node to remove.
     *
     * @note history deletes are not undoable.
     */
    _removeHistoryContainer: function(aContainerNode) {
      if (PlacesUtils.nodeIsHost(aContainerNode)) {
        // Site container.
        PlacesUtils.bhistory.removePagesFromHost(aContainerNode.title, true);
      }
      else if (PlacesUtils.nodeIsDay(aContainerNode)) {
        // Day container.
        let query = aContainerNode.getQueries()[0];
        let beginTime = query.beginTime;
        let endTime = query.endTime;
        NS_ASSERT(query && beginTime && endTime,
                  "A valid date container query should exist!");
        // We want to exclude beginTime from the removal because
        // removePagesByTimeframe includes both extremes, while date containers
        // exclude the lower extreme.  So, if we would not exclude it, we would
        // end up removing more history than requested.
        PlacesUtils.bhistory.removePagesByTimeframe(beginTime + 1, endTime);
      }
    },
  
    /**
     * Removes the selection
     * @param   aTxnName
     *          A name for the transaction if this is being performed
     *          as part of another operation.
     */
    remove: function(aTxnName) {
      if (!this._hasRemovableSelection())
        return;
  
      NS_ASSERT(aTxnName !== undefined, "Must supply Transaction Name");
  
      var root = this._view.result.root;
  
      if (PlacesUtils.nodeIsFolder(root))
        this._removeRowsFromBookmarks(aTxnName);
      else if (PlacesUtils.nodeIsQuery(root)) {
        var queryType = PlacesUtils.asQuery(root).queryOptions.queryType;
        if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS)
          this._removeRowsFromBookmarks(aTxnName);
        else if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY)
          this._removeRowsFromHistory();
        else
          NS_ASSERT(false, "implement support for QUERY_TYPE_UNIFIED");
      }
      else
        NS_ASSERT(false, "unexpected root");
    },
  
    /**
     * Fills a DataTransfer object with the content of the selection that can be
     * dropped elsewhere.
     * @param   aEvent
     *          The dragstart event.
     */
    setDataTransfer: function(aEvent) {
      let dt = aEvent.dataTransfer;
      let doCopy = ["copyLink", "copy", "link"].indexOf(dt.effectAllowed) != -1;
  
      let result = this._view.result;
      let didSuppressNotifications = result.suppressNotifications;
      if (!didSuppressNotifications)
        result.suppressNotifications = true;
  
      function addData(type, index, feedURI) {
        let wrapNode = PlacesUtils.wrapNode(node, type, feedURI);
        dt.mozSetDataAt(type, wrapNode, index);
      }
  
      function addURIData(index, feedURI) {
        addData(PlacesUtils.TYPE_X_MOZ_URL, index, feedURI);
        addData(PlacesUtils.TYPE_UNICODE, index, feedURI);
        addData(PlacesUtils.TYPE_HTML, index, feedURI);
      }
  
      try {
        let nodes = this._view.draggableSelection;
        for (let i = 0; i < nodes.length; ++i) {
          var node = nodes[i];
  
          // This order is _important_! It controls how this and other
          // applications select data to be inserted based on type.
          addData(PlacesUtils.TYPE_X_MOZ_PLACE, i);
  
          // Drop the feed uri for livemark containers
          let livemarkInfo = this.getCachedLivemarkInfo(node);
          if (livemarkInfo) {
            addURIData(i, livemarkInfo.feedURI.spec);
          }
          else if (node.uri) {
            addURIData(i);
          }
        }
      }
      finally {
        if (!didSuppressNotifications)
          result.suppressNotifications = false;
      }
    },
  
    get clipboardAction () {
      let action = {};
      let actionOwner;
      try {
        let xferable = Cc["@mozilla.org/widget/transferable;1"].
                       createInstance(Ci.nsITransferable);
        xferable.init(null);
        xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION)
        this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
        xferable.getTransferData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, action, {});
        [action, actionOwner] =
          action.value.QueryInterface(Ci.nsISupportsString).data.split(",");
      } catch(ex) {
        // Paste from external sources don't have any associated action, just
        // fallback to a copy action.
        return "copy";
      }
      // For cuts also check who inited the action, since cuts across different
      // instances should instead be handled as copies (The sources are not
      // available for this instance).
      if (action == "cut" && actionOwner != this.profileName)
        action = "copy";
  
      return action;
    },
  
    _releaseClipboardOwnership: function() {
      if (this.cutNodes.length > 0) {
        // This clears the logical clipboard, doesn't remove data.
        this.clipboard.emptyClipboard(Ci.nsIClipboard.kGlobalClipboard);
      }
    },
  
    _clearClipboard: function() {
      let xferable = Cc["@mozilla.org/widget/transferable;1"].
                     createInstance(Ci.nsITransferable);
      xferable.init(null);
      // Empty transferables may cause crashes, so just add an unknown type.
      const TYPE = "text/x-moz-place-empty";
      xferable.addDataFlavor(TYPE);
      xferable.setTransferData(TYPE, PlacesUtils.toISupportsString(""), 0);
      this.clipboard.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard);
    },
  
    _populateClipboard: function(aNodes, aAction) {
      // This order is _important_! It controls how this and other applications
      // select data to be inserted based on type.
      let contents = [
        { type: PlacesUtils.TYPE_X_MOZ_PLACE, entries: [] },
        { type: PlacesUtils.TYPE_X_MOZ_URL, entries: [] },
        { type: PlacesUtils.TYPE_HTML, entries: [] },
        { type: PlacesUtils.TYPE_UNICODE, entries: [] },
      ];
  
      // Avoid handling descendants of a copied node, the transactions take care
      // of them automatically.
      let copiedFolders = [];
      aNodes.forEach(function(node) {
        if (this._shouldSkipNode(node, copiedFolders))
          return;
        if (PlacesUtils.nodeIsFolder(node))
          copiedFolders.push(node);
  
        let livemarkInfo = this.getCachedLivemarkInfo(node);
        let feedURI = livemarkInfo && livemarkInfo.feedURI.spec;
  
        contents.forEach(function(content) {
          content.entries.push(
            PlacesUtils.wrapNode(node, content.type, feedURI)
          );
        });
      }, this);
  
      function addData(type, data) {
        xferable.addDataFlavor(type);
        xferable.setTransferData(type, PlacesUtils.toISupportsString(data),
                                 data.length * 2);
      }
  
      let xferable = Cc["@mozilla.org/widget/transferable;1"].
                     createInstance(Ci.nsITransferable);
      xferable.init(null);
      let hasData = false;
      // This order matters here!  It controls how this and other applications
      // select data to be inserted based on type.
      contents.forEach(function(content) {
        if (content.entries.length > 0) {
          hasData = true;
          let glue =
            content.type == PlacesUtils.TYPE_X_MOZ_PLACE ? "," : PlacesUtils.endl;
          addData(content.type, content.entries.join(glue));
        }
      });
  
      // Track the exected action in the xferable.  This must be the last flavor
      // since it's the least preferred one.
      // Enqueue a unique instance identifier to distinguish operations across
      // concurrent instances of the application.
      addData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, aAction + "," + this.profileName);
  
      if (hasData) {
        this.clipboard.setData(xferable,
                               this.cutNodes.length > 0 ? this : null,
                               Ci.nsIClipboard.kGlobalClipboard);
      }
    },
  
    _cutNodes: [],
    get cutNodes() this._cutNodes,
    set cutNodes(aNodes) {
      let self = this;
      function updateCutNodes(aValue) {
        self._cutNodes.forEach(function(aNode) {
          self._view.toggleCutNode(aNode, aValue);
        });
      }
  
      updateCutNodes(false);
      this._cutNodes = aNodes;
      updateCutNodes(true);
      return aNodes;
    },
  
    /**
     * Copy Bookmarks and Folders to the clipboard
     */
    copy: function() {
      let result = this._view.result;
      let didSuppressNotifications = result.suppressNotifications;
      if (!didSuppressNotifications)
        result.suppressNotifications = true;
      try {
        this._populateClipboard(this._view.selectedNodes, "copy");
      }
      finally {
        if (!didSuppressNotifications)
          result.suppressNotifications = false;
      }
    },
  
    /**
     * Cut Bookmarks and Folders to the clipboard
     */
    cut: function() {
      let result = this._view.result;
      let didSuppressNotifications = result.suppressNotifications;
      if (!didSuppressNotifications)
        result.suppressNotifications = true;
      try {
        this._populateClipboard(this._view.selectedNodes, "cut");
        this.cutNodes = this._view.selectedNodes;
      }
      finally {
        if (!didSuppressNotifications)
          result.suppressNotifications = false;
      }
    },
  
    /**
     * Paste Bookmarks and Folders from the clipboard
     */
    paste: function() {
      // No reason to proceed if there isn't a valid insertion point.
      let ip = this._view.insertionPoint;
      if (!ip)
        throw Cr.NS_ERROR_NOT_AVAILABLE;
  
      let action = this.clipboardAction;
  
      let xferable = Cc["@mozilla.org/widget/transferable;1"].
                     createInstance(Ci.nsITransferable);
      xferable.init(null);
      // This order matters here!  It controls the preferred flavors for this
      // paste operation.
      [ PlacesUtils.TYPE_X_MOZ_PLACE,
        PlacesUtils.TYPE_X_MOZ_URL,
        PlacesUtils.TYPE_UNICODE,
      ].forEach(function(type) xferable.addDataFlavor(type));
  
      this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
  
      // Now get the clipboard contents, in the best available flavor.
      let data = {}, type = {}, items = [];
      try {
        xferable.getAnyTransferData(type, data, {});
        data = data.value.QueryInterface(Ci.nsISupportsString).data;
        type = type.value;
        items = PlacesUtils.unwrapNodes(data, type);
      } catch(ex) {
        // No supported data exists or nodes unwrap failed, just bail out.
        return;
      }
  
      let transactions = [];
      let insertionIndex = ip.index;
      for (let i = 0; i < items.length; ++i) {
        if (ip.isTag) {
          // Pasting into a tag container means tagging the item, regardless of
          // the requested action.
          let tagTxn = new PlacesTagURITransaction(NetUtil.newURI(items[i].uri),
                                                   [ip.itemId]);
          transactions.push(tagTxn);
          continue;
        }
  
        // Adjust index to make sure items are pasted in the correct position.
        // If index is DEFAULT_INDEX, items are just appended.
        if (ip.index != PlacesUtils.bookmarks.DEFAULT_INDEX)
          insertionIndex = ip.index + i;
  
        transactions.push(
          PlacesUIUtils.makeTransaction(items[i], type, ip.itemId,
                                        insertionIndex, action == "copy")
        );
      }
  
      let aggregatedTxn = new PlacesAggregatedTransaction("Paste", transactions);
      PlacesUtils.transactionManager.doTransaction(aggregatedTxn);
  
      // Cut/past operations are not repeatable, so clear the clipboard.
      if (action == "cut") {
        this._clearClipboard();
      }
  
      // Select the pasted items, they should be consecutive.
      let insertedNodeIds = [];
      for (let i = 0; i < transactions.length; ++i) {
        insertedNodeIds.push(
          PlacesUtils.bookmarks.getIdForItemAt(ip.itemId, ip.index + i)
        );
      }
      if (insertedNodeIds.length > 0)
        this._view.selectItems(insertedNodeIds, false);
    },
  
    /**
     * Cache the livemark info for a node.  This allows the controller and the
     * views to treat the given node as a livemark.
     * @param aNode
     *        a places result node.
     * @param aLivemarkInfo
     *        a mozILivemarkInfo object.
     */
    cacheLivemarkInfo: function(aNode, aLivemarkInfo) {
      this._cachedLivemarkInfoObjects.set(aNode, aLivemarkInfo);
    },
  
    /**
     * Returns whether or not there's cached mozILivemarkInfo object for a node.
     * @param aNode
     *        a places result node.
     * @return true if there's a cached mozILivemarkInfo object for
     * aNode, false otherwise.
     */
    hasCachedLivemarkInfo: function(aNode)
      this._cachedLivemarkInfoObjects.has(aNode),
  
    /**
     * Returns the cached livemark info for a node, if set by cacheLivemarkInfo,
     * null otherwise.
     * @param aNode
     *        a places result node.
     * @return the mozILivemarkInfo object for aNode, if set, null otherwise.
     */
    getCachedLivemarkInfo: function(aNode)
      this._cachedLivemarkInfoObjects.get(aNode, null)
  };
  
  /**
   * Handles drag and drop operations for views. Note that this is view agnostic!
   * You should not use PlacesController._view within these methods, since
   * the view that the item(s) have been dropped on was not necessarily active.
   * Drop functions are passed the view that is being dropped on.
   */
  var PlacesControllerDragHelper = {
    /**
     * DOM Element currently being dragged over
     */
    currentDropTarget: null,
  
    /**
     * Determines if the mouse is currently being dragged over a child node of
     * this menu. This is necessary so that the menu doesn't close while the
     * mouse is dragging over one of its submenus
     * @param   node
     *          The container node
     * @returns true if the user is dragging over a node within the hierarchy of
     *          the container, false otherwise.
     */
    draggingOverChildNode: function(node) {
      let currentNode = this.currentDropTarget;
      while (currentNode) {
        if (currentNode == node)
          return true;
        currentNode = currentNode.parentNode;
      }
      return false;
    },
  
    /**
     * @returns The current active drag session. Returns null if there is none.
     */
    getSession: function() {
      return this.dragService.getCurrentSession();
    },
  
    /**
     * Extract the first accepted flavor from a list of flavors.
     * @param aFlavors
     *        The flavors list of type nsIDOMDOMStringList.
     */
    getFirstValidFlavor: function(aFlavors) {
      for (let i = 0; i < aFlavors.length; i++) {
        if (this.GENERIC_VIEW_DROP_TYPES.indexOf(aFlavors[i]) != -1)
          return aFlavors[i];
      }
  
      // If no supported flavor is found, check if data includes text/plain
      // contents.  If so, request them as text/unicode, a conversion will happen
      // automatically.
      if (aFlavors.contains("text/plain")) {
          return PlacesUtils.TYPE_UNICODE;
      }
  
      return null;
    },
  
    /**
     * Determines whether or not the data currently being dragged can be dropped
     * on a places view.
     * @param ip
     *        The insertion point where the items should be dropped.
     */
    canDrop: function(ip, dt) {
      let dropCount = dt.mozItemCount;
  
      // Check every dragged item.
      for (let i = 0; i < dropCount; i++) {
        let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i));
        if (!flavor)
          return false;
  
        // Urls can be dropped on any insertionpoint.
        // XXXmano: remember that this method is called for each dragover event!
        // Thus we shouldn't use unwrapNodes here at all if possible.
        // I think it would be OK to accept bogus data here (e.g. text which was
        // somehow wrapped as TAB_DROP_TYPE, this is not in our control, and
        // will just case the actual drop to be a no-op), and only rule out valid
        // expected cases, which are either unsupported flavors, or items which
        // cannot be dropped in the current insertionpoint. The last case will
        // likely force us to use unwrapNodes for the private data types of
        // places.
        if (flavor == TAB_DROP_TYPE)
          continue;
  
        let data = dt.mozGetDataAt(flavor, i);
        let dragged;
        try {
          dragged = PlacesUtils.unwrapNodes(data, flavor)[0];
        }
        catch (e) {
          return false;
        }
  
        // Only bookmarks and urls can be dropped into tag containers.
        if (ip.isTag && ip.orientation == Ci.nsITreeView.DROP_ON &&
            dragged.type != PlacesUtils.TYPE_X_MOZ_URL &&
            (dragged.type != PlacesUtils.TYPE_X_MOZ_PLACE ||
             (dragged.uri && dragged.uri.startsWith("place:")) ))
          return false;
  
        // The following loop disallows the dropping of a folder on itself or
        // on any of its descendants.
        if (dragged.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER ||
            (dragged.uri && dragged.uri.startsWith("place:")) ) {
          let parentId = ip.itemId;
          while (parentId != PlacesUtils.placesRootId) {
            if (dragged.concreteId == parentId || dragged.id == parentId)
              return false;
            parentId = PlacesUtils.bookmarks.getFolderIdForItem(parentId);
          }
        }
      }
      return true;
    },
  
  
    /**
     * Determines if a node can be moved.
     *
     * @param   aNode
     *          A nsINavHistoryResultNode node.
     * @returns True if the node can be moved, false otherwise.
     */
    canMoveNode:
    function(aNode) {
      // Only bookmark items are movable.
      if (aNode.itemId == -1)
        return false;
  
      // Once tags and bookmarked are divorced, the tag-query check should be
      // removed.
      let parentNode = aNode.parent;
      return parentNode != null &&
             !(PlacesUtils.nodeIsFolder(parentNode) &&
               PlacesUIUtils.isContentsReadOnly(parentNode)) &&
             !PlacesUtils.nodeIsTagQuery(parentNode);
    },
  
    /**
     * Handles the drop of one or more items onto a view.
     * @param   insertionPoint
     *          The insertion point where the items should be dropped
     */
    onDrop: function(insertionPoint, dt) {
      let doCopy = ["copy", "link"].indexOf(dt.dropEffect) != -1;
  
      let transactions = [];
      let dropCount = dt.mozItemCount;
      let movedCount = 0;
      for (let i = 0; i < dropCount; ++i) {
        let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i));
        if (!flavor)
          return;
  
        let data = dt.mozGetDataAt(flavor, i);
        let unwrapped;
        if (flavor != TAB_DROP_TYPE) {
          // There's only ever one in the D&D case.
          unwrapped = PlacesUtils.unwrapNodes(data, flavor)[0];
        }
        else if (data instanceof XULElement && data.localName == "tab" &&
                 data.ownerDocument.defaultView instanceof ChromeWindow) {
          let uri = data.linkedBrowser.currentURI;
          let spec = uri ? uri.spec : "about:blank";
          let title = data.label;
          unwrapped = { uri: spec,
                        title: data.label,
                        type: PlacesUtils.TYPE_X_MOZ_URL};
        }
        else
          throw("bogus data was passed as a tab");
  
        let index = insertionPoint.index;
  
        // Adjust insertion index to prevent reversal of dragged items. When you
        // drag multiple elts upward: need to increment index or each successive
        // elt will be inserted at the same index, each above the previous.
        let dragginUp = insertionPoint.itemId == unwrapped.parent &&
                        index < PlacesUtils.bookmarks.getItemIndex(unwrapped.id);
        if (index != -1 && dragginUp)
          index += movedCount++;
  
        // If dragging over a tag container we should tag the item.
        if (insertionPoint.isTag &&
            insertionPoint.orientation == Ci.nsITreeView.DROP_ON) {
          let uri = NetUtil.newURI(unwrapped.uri);
          let tagItemId = insertionPoint.itemId;
          let tagTxn = new PlacesTagURITransaction(uri, [tagItemId]);
          transactions.push(tagTxn);
        }
        else {
          transactions.push(PlacesUIUtils.makeTransaction(unwrapped,
                            flavor, insertionPoint.itemId,
                            index, doCopy));
        }
      }
  
      let txn = new PlacesAggregatedTransaction("DropItems", transactions);
      PlacesUtils.transactionManager.doTransaction(txn);
    },
  
    /**
     * Checks if we can insert into a container.
     * @param   aContainer
     *          The container were we are want to drop
     */
    disallowInsertion: function(aContainer) {
      NS_ASSERT(aContainer, "empty container");
      // Allow dropping into Tag containers and editable folders.
      return !PlacesUtils.nodeIsTagQuery(aContainer) &&
             (!PlacesUtils.nodeIsFolder(aContainer) ||
              PlacesUIUtils.isContentsReadOnly(aContainer));
    },
  
    placesFlavors: [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
                    PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
                    PlacesUtils.TYPE_X_MOZ_PLACE],
  
    // The order matters.
    GENERIC_VIEW_DROP_TYPES: [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
                              PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
                              PlacesUtils.TYPE_X_MOZ_PLACE,
                              PlacesUtils.TYPE_X_MOZ_URL,
                              TAB_DROP_TYPE,
                              PlacesUtils.TYPE_UNICODE],
  };
  
  
  XPCOMUtils.defineLazyServiceGetter(PlacesControllerDragHelper, "dragService",
                                     "@mozilla.org/widget/dragservice;1",
                                     "nsIDragService");
  
  function goUpdatePlacesCommands() {
    // Get the controller for one of the places commands.
    var placesController = doGetPlacesControllerForCommand("placesCmd_open");
    function updatePlacesCommand(aCommand) {
      goSetCommandEnabled(aCommand, placesController &&
                                    placesController.isCommandEnabled(aCommand));
    }
  
    updatePlacesCommand("placesCmd_open");
    updatePlacesCommand("placesCmd_open:window");
    updatePlacesCommand("placesCmd_open:privatewindow");
    updatePlacesCommand("placesCmd_open:tab");
    updatePlacesCommand("placesCmd_new:folder");
    updatePlacesCommand("placesCmd_new:bookmark");
    updatePlacesCommand("placesCmd_new:livemark");
    updatePlacesCommand("placesCmd_new:separator");
    updatePlacesCommand("placesCmd_show:info");
    updatePlacesCommand("placesCmd_moveBookmarks");
    updatePlacesCommand("placesCmd_reload");
    updatePlacesCommand("placesCmd_sortBy:name");
    updatePlacesCommand("placesCmd_openParentFolder");
    updatePlacesCommand("placesCmd_cut");
    updatePlacesCommand("placesCmd_copy");
    updatePlacesCommand("placesCmd_paste");
    updatePlacesCommand("placesCmd_delete");
  }
  
  function doGetPlacesControllerForCommand(aCommand)
  {
    // A context menu may be built for non-focusable views.  Thus, we first try
    // to look for a view associated with document.popupNode
    let popupNode;
    try {
      popupNode = document.popupNode;
    } catch (e) {
      // The document went away (bug 797307).
      return null;
    }
    if (popupNode) {
      let view = PlacesUIUtils.getViewForNode(popupNode);
      if (view && view._contextMenuShown)
        return view.controllers.getControllerForCommand(aCommand);
    }
  
    // When we're not building a context menu, only focusable views
    // are possible.  Thus, we can safely use the command dispatcher.
    let controller = top.document.commandDispatcher
                        .getControllerForCommand(aCommand);
    if (controller)
      return controller;
  
    return null;
  }
  
  function goDoPlacesCommand(aCommand)
  {
    let controller = doGetPlacesControllerForCommand(aCommand);
    if (controller && controller.isCommandEnabled(aCommand))
      controller.doCommand(aCommand);
  }
  
  