tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

controller.js (57222B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
      5 
      6 ChromeUtils.defineESModuleGetters(this, {
      7  PlacesTransactions: "resource://gre/modules/PlacesTransactions.sys.mjs",
      8  PlacesUIUtils: "moz-src:///browser/components/places/PlacesUIUtils.sys.mjs",
      9  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     10 });
     11 
     12 /* import-globals-from /browser/base/content/utilityOverlay.js */
     13 /* import-globals-from ./places.js */
     14 
     15 /**
     16 * Represents an insertion point within a container where we can insert
     17 * items.
     18 *
     19 * @param {object} options an object containing the following properties:
     20 * @param {string} options.parentGuid
     21 *     The unique identifier of the parent container
     22 * @param {number} [options.index]
     23 *     The index within the container where to insert, defaults to appending
     24 * @param {number} [options.orientation]
     25 *     The orientation of the insertion. NOTE: the adjustments to the
     26 *     insertion point to accommodate the orientation should be done by
     27 *     the person who constructs the IP, not the user. The orientation
     28 *     is provided for informational purposes only! Defaults to DROP_ON.
     29 * @param {string} [options.tagName]
     30 *     The tag name if this IP is set to a tag, null otherwise.
     31 * @param {*} [options.dropNearNode]
     32 *     When defined index will be calculated based on this node
     33 */
     34 function PlacesInsertionPoint({
     35  parentGuid,
     36  index = PlacesUtils.bookmarks.DEFAULT_INDEX,
     37  orientation = Ci.nsITreeView.DROP_ON,
     38  tagName = null,
     39  dropNearNode = null,
     40 }) {
     41  this.guid = parentGuid;
     42  this._index = index;
     43  this.orientation = orientation;
     44  this.tagName = tagName;
     45  this.dropNearNode = dropNearNode;
     46 }
     47 
     48 PlacesInsertionPoint.prototype = {
     49  set index(val) {
     50    this._index = val;
     51  },
     52 
     53  async getIndex() {
     54    if (this.dropNearNode) {
     55      // If dropNearNode is set up we must calculate the index of the item near
     56      // which we will drop.
     57      let index = (
     58        await PlacesUtils.bookmarks.fetch(this.dropNearNode.bookmarkGuid)
     59      ).index;
     60      return this.orientation == Ci.nsITreeView.DROP_BEFORE ? index : index + 1;
     61    }
     62    return this._index;
     63  },
     64 
     65  get isTag() {
     66    return typeof this.tagName == "string";
     67  },
     68 };
     69 
     70 /**
     71 * Places Controller
     72 */
     73 
     74 function PlacesController(aView) {
     75  this._view = aView;
     76  ChromeUtils.defineLazyGetter(this, "profileName", function () {
     77    return Services.dirsvc.get("ProfD", Ci.nsIFile).leafName;
     78  });
     79 
     80  ChromeUtils.defineESModuleGetters(this, {
     81    ForgetAboutSite: "resource://gre/modules/ForgetAboutSite.sys.mjs",
     82  });
     83 }
     84 
     85 PlacesController.prototype = {
     86  /**
     87   * The places view.
     88   */
     89  _view: null,
     90 
     91  // This is used in certain views to disable user actions on the places tree
     92  // views. This avoids accidental deletion/modification when the user is not
     93  // actually organising the trees.
     94  disableUserActions: false,
     95 
     96  QueryInterface: ChromeUtils.generateQI(["nsIClipboardOwner"]),
     97 
     98  // nsIClipboardOwner
     99  LosingOwnership: function PC_LosingOwnership() {
    100    this.cutNodes = [];
    101  },
    102 
    103  terminate: function PC_terminate() {
    104    this._releaseClipboardOwnership();
    105  },
    106 
    107  supportsCommand: function PC_supportsCommand(aCommand) {
    108    if (this.disableUserActions) {
    109      return false;
    110    }
    111    // Non-Places specific commands that we also support
    112    switch (aCommand) {
    113      case "cmd_undo":
    114      case "cmd_redo":
    115      case "cmd_cut":
    116      case "cmd_copy":
    117      case "cmd_paste":
    118      case "cmd_delete":
    119      case "cmd_selectAll":
    120        return true;
    121    }
    122 
    123    // All other Places Commands are prefixed with "placesCmd_" ... this
    124    // filters out other commands that we do _not_ support (see 329587).
    125    const CMD_PREFIX = "placesCmd_";
    126    return aCommand.substr(0, CMD_PREFIX.length) == CMD_PREFIX;
    127  },
    128 
    129  isCommandEnabled: function PC_isCommandEnabled(aCommand) {
    130    // Determine whether or not nodes can be inserted.
    131    let ip = this._view.insertionPoint;
    132    let canInsert = ip && (aCommand.endsWith("_paste") || !ip.isTag);
    133 
    134    switch (aCommand) {
    135      case "cmd_undo":
    136        return PlacesTransactions.topUndoEntry != null;
    137      case "cmd_redo":
    138        return PlacesTransactions.topRedoEntry != null;
    139      case "cmd_cut":
    140      case "placesCmd_cut":
    141        for (let node of this._view.selectedNodes) {
    142          // If selection includes history nodes or tags-as-bookmark, disallow
    143          // cutting.
    144          if (
    145            node.itemId == -1 ||
    146            (node.parent && PlacesUtils.nodeIsTagQuery(node.parent))
    147          ) {
    148            return false;
    149          }
    150        }
    151      // Otherwise fall through the cmd_delete check.
    152      case "cmd_delete":
    153      case "placesCmd_delete":
    154      case "placesCmd_deleteDataHost":
    155        return this._hasRemovableSelection();
    156      case "cmd_copy":
    157      case "placesCmd_copy":
    158      case "placesCmd_showInFolder":
    159        return this._view.hasSelection;
    160      case "cmd_paste":
    161      case "placesCmd_paste":
    162        // If the clipboard contains a Places flavor it is definitely pasteable,
    163        // otherwise we also allow pasting "text/plain" and "text/x-moz-url" data.
    164        // We don't check if the data is valid here, because the clipboard may
    165        // contain very large blobs that would largely slowdown commands updating.
    166        // Of course later paste() should ignore any invalid data.
    167        return (
    168          canInsert &&
    169          Services.clipboard.hasDataMatchingFlavors(
    170            [
    171              ...PlacesUIUtils.PLACES_FLAVORS,
    172              PlacesUtils.TYPE_X_MOZ_URL,
    173              PlacesUtils.TYPE_PLAINTEXT,
    174            ],
    175            Ci.nsIClipboard.kGlobalClipboard
    176          )
    177        );
    178      case "cmd_selectAll":
    179        if (this._view.selType != "single") {
    180          let rootNode = this._view.result.root;
    181          if (rootNode.containerOpen && rootNode.childCount > 0) {
    182            return true;
    183          }
    184        }
    185        return false;
    186      case "placesCmd_open":
    187      case "placesCmd_open:window":
    188      case "placesCmd_open:privatewindow":
    189      case "placesCmd_open:tab": {
    190        let selectedNode = this._view.selectedNode;
    191        return selectedNode && PlacesUtils.nodeIsURI(selectedNode);
    192      }
    193      case "placesCmd_new:folder":
    194        return canInsert;
    195      case "placesCmd_new:bookmark":
    196        return canInsert;
    197      case "placesCmd_new:separator":
    198        return (
    199          canInsert &&
    200          !PlacesUtils.asQuery(this._view.result.root).queryOptions
    201            .excludeItems &&
    202          this._view.result.sortingMode ==
    203            Ci.nsINavHistoryQueryOptions.SORT_BY_NONE
    204        );
    205      case "placesCmd_show:info": {
    206        let selectedNode = this._view.selectedNode;
    207        return (
    208          selectedNode &&
    209          !PlacesUtils.isRootItem(
    210            PlacesUtils.getConcreteItemGuid(selectedNode)
    211          ) &&
    212          (PlacesUtils.nodeIsTagQuery(selectedNode) ||
    213            PlacesUtils.nodeIsBookmark(selectedNode) ||
    214            (PlacesUtils.nodeIsFolderOrShortcut(selectedNode) &&
    215              !PlacesUtils.nodeIsQueryGeneratedFolder(selectedNode)))
    216        );
    217      }
    218      case "placesCmd_sortBy:name": {
    219        let selectedNode = this._view.selectedNode;
    220        return (
    221          selectedNode &&
    222          PlacesUtils.nodeIsFolderOrShortcut(selectedNode) &&
    223          !PlacesUIUtils.isFolderReadOnly(selectedNode) &&
    224          this._view.result.sortingMode ==
    225            Ci.nsINavHistoryQueryOptions.SORT_BY_NONE
    226        );
    227      }
    228      case "placesCmd_createBookmark": {
    229        return !this._view.selectedNodes.some(
    230          node => !PlacesUtils.nodeIsURI(node) || node.itemId != -1
    231        );
    232      }
    233      default:
    234        return false;
    235    }
    236  },
    237 
    238  doCommand: function PC_doCommand(aCommand) {
    239    if (aCommand != "cmd_delete" && aCommand != "placesCmd_delete") {
    240      // Clear out last removal fingerprint if any other commands arrives.
    241      // This covers sequences like: remove, undo, remove, where the removal
    242      // commands are not immediately adjacent.
    243      this._lastRemoveOperationFingerprint = null;
    244    }
    245    switch (aCommand) {
    246      case "cmd_undo":
    247        PlacesTransactions.undo().catch(console.error);
    248        break;
    249      case "cmd_redo":
    250        PlacesTransactions.redo().catch(console.error);
    251        break;
    252      case "cmd_cut":
    253      case "placesCmd_cut":
    254        this.cut();
    255        break;
    256      case "cmd_copy":
    257      case "placesCmd_copy":
    258        this.copy();
    259        break;
    260      case "cmd_paste":
    261      case "placesCmd_paste":
    262        this.paste().catch(console.error);
    263        break;
    264      case "cmd_delete":
    265      case "placesCmd_delete":
    266        this.remove("Remove Selection").catch(console.error);
    267        break;
    268      case "placesCmd_deleteDataHost":
    269        this.forgetAboutThisSite().catch(console.error);
    270        break;
    271      case "cmd_selectAll":
    272        this.selectAll();
    273        break;
    274      case "placesCmd_open":
    275        PlacesUIUtils.openNodeIn(
    276          this._view.selectedNode,
    277          "current",
    278          this._view
    279        );
    280        break;
    281      case "placesCmd_open:window":
    282        PlacesUIUtils.openNodeIn(this._view.selectedNode, "window", this._view);
    283        break;
    284      case "placesCmd_open:privatewindow":
    285        PlacesUIUtils.openNodeIn(
    286          this._view.selectedNode,
    287          "window",
    288          this._view,
    289          true
    290        );
    291        break;
    292      case "placesCmd_open:tab":
    293        PlacesUIUtils.openNodeIn(this._view.selectedNode, "tab", this._view);
    294        break;
    295      case "placesCmd_new:folder":
    296        this.newItem("folder").catch(console.error);
    297        break;
    298      case "placesCmd_new:bookmark":
    299        this.newItem("bookmark").catch(console.error);
    300        break;
    301      case "placesCmd_new:separator":
    302        this.newSeparator().catch(console.error);
    303        break;
    304      case "placesCmd_show:info":
    305        this.showBookmarkPropertiesForSelection();
    306        break;
    307      case "placesCmd_sortBy:name":
    308        this.sortFolderByName().catch(console.error);
    309        break;
    310      case "placesCmd_createBookmark": {
    311        const nodes = this._view.selectedNodes.map(node => {
    312          return {
    313            uri: Services.io.newURI(node.uri),
    314            title: node.title,
    315          };
    316        });
    317        PlacesUIUtils.showBookmarkPagesDialog(
    318          nodes,
    319          ["keyword", "location"],
    320          window.top
    321        );
    322        break;
    323      }
    324      case "placesCmd_showInFolder":
    325        this.showInFolder(this._view.selectedNode.bookmarkGuid);
    326        break;
    327    }
    328  },
    329 
    330  onEvent: function PC_onEvent() {},
    331 
    332  /**
    333   * Determine whether or not the selection can be removed, either by the
    334   * delete or cut operations based on whether or not any of its contents
    335   * are non-removable. We don't need to worry about recursion here since it
    336   * is a policy decision that a removable item not be placed inside a non-
    337   * removable item.
    338   *
    339   * @returns {boolean} true if all nodes in the selection can be removed,
    340   *         false otherwise.
    341   */
    342  _hasRemovableSelection() {
    343    var ranges = this._view.removableSelectionRanges;
    344    if (!ranges.length) {
    345      return false;
    346    }
    347 
    348    var root = this._view.result.root;
    349 
    350    for (var j = 0; j < ranges.length; j++) {
    351      var nodes = ranges[j];
    352      for (var i = 0; i < nodes.length; ++i) {
    353        // Disallow removing the view's root node
    354        if (nodes[i] == root) {
    355          return false;
    356        }
    357 
    358        if (!PlacesUIUtils.canUserRemove(nodes[i])) {
    359          return false;
    360        }
    361      }
    362    }
    363 
    364    return true;
    365  },
    366 
    367  /**
    368   * This helper can be used to avoid handling repeated remove operations.
    369   * Clear this._lastRemoveOperationFingerprint if another operation happens.
    370   *
    371   * @returns {boolean} whether the removal is the same as the last one.
    372   */
    373  _isRepeatedRemoveOperation() {
    374    let lastRemoveOperationFingerprint = this._lastRemoveOperationFingerprint;
    375    // .bookmarkGuid and .pageGuid may either be null or an empty string. While
    376    // that should probably change, it's safer to use || here.
    377    this._lastRemoveOperationFingerprint = PlacesUtils.sha256(
    378      this._view.selectedNodes
    379        .map(n => n.bookmarkGuid || (n.pageGuid || n.uri) + n.time)
    380        .join()
    381    );
    382    return (
    383      lastRemoveOperationFingerprint == this._lastRemoveOperationFingerprint
    384    );
    385  },
    386 
    387  /**
    388   * Gathers information about the selected nodes according to the following
    389   * rules:
    390   *    "link"              node is a URI
    391   *    "bookmark"          node is a bookmark
    392   *    "tagChild"          node is a child of a tag
    393   *    "folder"            node is a folder
    394   *    "query"             node is a query
    395   *    "separator"         node is a separator line
    396   *    "host"              node is a host
    397   *
    398   * @returns {Array} an array of objects corresponding the selected nodes. Each
    399   *         object has each of the properties above set if its corresponding
    400   *         node matches the rule. In addition, the annotations names for each
    401   *         node are set on its corresponding object as properties.
    402   * Notes:
    403   *   1) This can be slow, so don't call it anywhere performance critical!
    404   */
    405  _buildSelectionMetadata() {
    406    return this._view.selectedNodes.map(n => this._selectionMetadataForNode(n));
    407  },
    408 
    409  _selectionMetadataForNode(node) {
    410    let nodeData = {};
    411    // We don't use the nodeIs* methods here to avoid going through the type
    412    // property way too often
    413    switch (node.type) {
    414      case Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY:
    415        nodeData.query = true;
    416        if (node.parent) {
    417          switch (PlacesUtils.asQuery(node.parent).queryOptions.resultType) {
    418            case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY:
    419              nodeData.query_host = true;
    420              break;
    421            case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY:
    422            case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY:
    423              nodeData.query_day = true;
    424              break;
    425            case Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAGS_ROOT:
    426              nodeData.query_tag = true;
    427          }
    428        }
    429        break;
    430      case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER:
    431      case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT:
    432        nodeData.folder = true;
    433        break;
    434      case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR:
    435        nodeData.separator = true;
    436        break;
    437      case Ci.nsINavHistoryResultNode.RESULT_TYPE_URI:
    438        nodeData.link = true;
    439        if (PlacesUtils.nodeIsBookmark(node)) {
    440          nodeData.link_bookmark = true;
    441          var parentNode = node.parent;
    442          if (parentNode && PlacesUtils.nodeIsTagQuery(parentNode)) {
    443            nodeData.link_bookmark_tag = true;
    444          }
    445        }
    446        break;
    447    }
    448    return nodeData;
    449  },
    450 
    451  /**
    452   * Determines if a context-menu item should be shown
    453   *
    454   * @param {object} aMenuItem
    455   *        the context menu item
    456   * @param {object} aMetaData
    457   *        meta data about the selection
    458   * @returns {boolean} true if the conditions (see buildContextMenu) are satisfied
    459   *          and the item can be displayed, false otherwise.
    460   */
    461  _shouldShowMenuItem(aMenuItem, aMetaData) {
    462    if (PlacesUIUtils.shouldHideOpenMenuItem(aMenuItem)) {
    463      return false;
    464    }
    465 
    466    let selectiontype =
    467      aMenuItem.getAttribute("selection-type") || "single|multiple";
    468 
    469    var selectionTypes = selectiontype.split("|");
    470    if (selectionTypes.includes("any")) {
    471      return true;
    472    }
    473    var count = aMetaData.length;
    474    if (count > 1 && !selectionTypes.includes("multiple")) {
    475      return false;
    476    }
    477    if (count == 1 && !selectionTypes.includes("single")) {
    478      return false;
    479    }
    480    // If there is no selection and selectionType doesn't include `none`
    481    // hide the item, otherwise try to use the root node to extract valid
    482    // metadata to compare against.
    483    if (count == 0) {
    484      if (!selectionTypes.includes("none")) {
    485        return false;
    486      }
    487      aMetaData = [this._selectionMetadataForNode(this._view.result.root)];
    488    }
    489 
    490    let attr = aMenuItem.getAttribute("hide-if-node-type");
    491    if (attr) {
    492      let rules = attr.split("|");
    493      if (aMetaData.some(d => rules.some(r => r in d))) {
    494        return false;
    495      }
    496    }
    497 
    498    attr = aMenuItem.getAttribute("hide-if-node-type-is-only");
    499    if (attr) {
    500      let rules = attr.split("|");
    501      if (rules.some(r => aMetaData.every(d => r in d))) {
    502        return false;
    503      }
    504    }
    505 
    506    attr = aMenuItem.getAttribute("node-type");
    507    if (!attr) {
    508      return true;
    509    }
    510 
    511    let anyMatched = false;
    512    let rules = attr.split("|");
    513    for (let metaData of aMetaData) {
    514      if (rules.some(r => r in metaData)) {
    515        anyMatched = true;
    516      } else {
    517        return false;
    518      }
    519    }
    520    return anyMatched;
    521  },
    522 
    523  /**
    524   * Uses meta-data rules set as attributes on the menuitems, representing the
    525   * current selection in the view (see `_buildSelectionMetadata`) and sets the
    526   * visibility state for each menuitem according to the following rules:
    527   *  1) The visibility state is unchanged if none of the attributes are set.
    528   *  2) Attributes should not be set on menuseparators.
    529   *  3) The boolean `ignore-item` attribute may be set when this code should
    530   *     not handle that menuitem.
    531   *  4) The `selection-type` attribute may be set to:
    532   *      - `single` if it should be visible only when there is a single node
    533   *         selected
    534   *      - `multiple` if it should be visible only when multiple nodes are
    535   *         selected
    536   *      - `none` if it should be visible when there are no selected nodes
    537   *      - `any` if it should be visible for any kind of selection
    538   *      - a `|` separated combination of the above.
    539   *  5) The `node-type` attribute may be set to values representing the
    540   *     type of the node triggering the context menu. The menuitem will be
    541   *     visible when one of the rules (separated by `|`) matches.
    542   *     In case of multiple selection, the menuitem is visible only if all of
    543   *     the selected nodes match one of the rule.
    544   *  6) The `hide-if-node-type` accepts the same rules as `node-type`, but
    545   *     hides the menuitem if the nodes match at least one of the rules.
    546   *     It takes priority over `nodetype`.
    547   *  7) The `hide-if-node-type-is-only` accepts the same rules as `node-type`, but
    548   *     hides the menuitem if any of the rules match all of the nodes.
    549   *  8) The boolean `hide-if-no-insertion-point` attribute may be set to hide a
    550   *     menuitem when there's no insertion point. An insertion point represents
    551   *     a point in the view where a new item can be inserted.
    552   *  9) The boolean `hide-if-private-browsing` attribute may be set to hide a
    553   *     menuitem in private browsing mode.
    554   * 10) The boolean `hide-if-disabled-private-browsing` attribute may be set to
    555   *     hide a menuitem if private browsing is not enabled.
    556   * 11) The boolean `hide-if-usercontext-disabled` attribute may be set to
    557   *     hide a menuitem if containers are disabled.
    558   * 12) The boolean `hide-if-single-click-opens` attribute may be set to hide a
    559   *     menuitem in views opening entries with a single click.
    560   *
    561   * @param {object} aPopup
    562   *        The menupopup to build children into.
    563   * @returns {boolean} true if at least one item is visible, false otherwise.
    564   */
    565  buildContextMenu(aPopup) {
    566    var metadata = this._buildSelectionMetadata();
    567    var ip = this._view.insertionPoint;
    568    var noIp = !ip || ip.isTag;
    569 
    570    var separator = null;
    571    var visibleItemsBeforeSep = false;
    572    var usableItemCount = 0;
    573    for (var i = 0; i < aPopup.children.length; ++i) {
    574      var item = aPopup.children[i];
    575      if (item.getAttribute("ignore-item") == "true") {
    576        continue;
    577      }
    578      if (item.localName != "menuseparator") {
    579        // We allow pasting into tag containers, so special case that.
    580        let hideIfNoIP =
    581          item.getAttribute("hide-if-no-insertion-point") == "true" &&
    582          noIp &&
    583          !(ip && ip.isTag && item.id == "placesContext_paste");
    584        // Hide `Open` if the primary action on click is opening.
    585        let hideIfSingleClickOpens =
    586          item.getAttribute("hide-if-single-click-opens") == "true" &&
    587          !PlacesUIUtils.loadBookmarksInBackground &&
    588          !PlacesUIUtils.loadBookmarksInTabs &&
    589          this._view.singleClickOpens;
    590        let hideIfNotSearch =
    591          item.getAttribute("hide-if-not-search") == "true" &&
    592          (!this._view.selectedNode ||
    593            !this._view.selectedNode.parent ||
    594            !PlacesUtils.nodeIsQuery(this._view.selectedNode.parent));
    595 
    596        let shouldHideItem =
    597          hideIfNoIP ||
    598          hideIfSingleClickOpens ||
    599          hideIfNotSearch ||
    600          !this._shouldShowMenuItem(item, metadata);
    601        item.hidden = shouldHideItem;
    602        item.disabled =
    603          shouldHideItem || item.getAttribute("start-disabled") == "true";
    604 
    605        if (!item.hidden) {
    606          visibleItemsBeforeSep = true;
    607          usableItemCount++;
    608 
    609          // Show the separator above the menu-item if any
    610          if (separator) {
    611            separator.hidden = false;
    612            separator = null;
    613          }
    614        }
    615      } else {
    616        // menuseparator
    617        // Initially hide it. It will be unhidden if there will be at least one
    618        // visible menu-item above and below it.
    619        item.hidden = true;
    620 
    621        // We won't show the separator at all if no items are visible above it
    622        if (visibleItemsBeforeSep) {
    623          separator = item;
    624        }
    625 
    626        // New separator, count again:
    627        visibleItemsBeforeSep = false;
    628      }
    629 
    630      if (item.id === "placesContext_deleteBookmark") {
    631        document.l10n.setAttributes(item, "places-delete-bookmark", {
    632          count: metadata.length,
    633        });
    634      }
    635      if (item.id === "placesContext_deleteFolder") {
    636        document.l10n.setAttributes(item, "places-delete-folder", {
    637          count: metadata.length,
    638        });
    639      }
    640    }
    641 
    642    // Set Open Folder/Links In Tabs or Open Bookmark item's enabled state if they're visible
    643    if (usableItemCount > 0) {
    644      let openContainerInTabsItem = document.getElementById(
    645        "placesContext_openContainer:tabs"
    646      );
    647      let openBookmarksItem = document.getElementById(
    648        "placesContext_openBookmarkContainer:tabs"
    649      );
    650      for (let menuItem of [openContainerInTabsItem, openBookmarksItem]) {
    651        if (!menuItem.hidden) {
    652          var containerToUse =
    653            this._view.selectedNode || this._view.result.root;
    654          if (PlacesUtils.nodeIsContainer(containerToUse)) {
    655            if (!PlacesUtils.hasChildURIs(containerToUse)) {
    656              menuItem.disabled = true;
    657              // Ensure that we don't display the menu if nothing is enabled:
    658              usableItemCount--;
    659            }
    660          }
    661        }
    662      }
    663    }
    664 
    665    const deleteHistoryItem = document.getElementById(
    666      "placesContext_delete_history"
    667    );
    668    document.l10n.setAttributes(deleteHistoryItem, "places-delete-page", {
    669      count: metadata.length,
    670    });
    671 
    672    const createBookmarkItem = document.getElementById(
    673      "placesContext_createBookmark"
    674    );
    675    document.l10n.setAttributes(createBookmarkItem, "places-create-bookmark", {
    676      count: metadata.length,
    677    });
    678 
    679    return usableItemCount > 0;
    680  },
    681 
    682  /**
    683   * Select all links in the current view.
    684   */
    685  selectAll: function PC_selectAll() {
    686    this._view.selectAll();
    687  },
    688 
    689  /**
    690   * Opens the bookmark properties for the selected URI Node.
    691   */
    692  showBookmarkPropertiesForSelection() {
    693    let node = this._view.selectedNode;
    694    if (!node) {
    695      return;
    696    }
    697 
    698    PlacesUIUtils.showBookmarkDialog(
    699      { action: "edit", node, hiddenRows: ["folderPicker"] },
    700      window.top
    701    );
    702  },
    703 
    704  /**
    705   * Opens the links in the selected folder, or the selected links in new tabs.
    706   *
    707   * @param {object} aEvent
    708   *   The associated event.
    709   */
    710  openSelectionInTabs: function PC_openLinksInTabs(aEvent) {
    711    var node = this._view.selectedNode;
    712    var nodes = this._view.selectedNodes;
    713    // In the case of no selection, open the root node:
    714    if (!node && !nodes.length) {
    715      node = this._view.result.root;
    716    }
    717    PlacesUIUtils.openMultipleLinksInTabs(
    718      node ? node : nodes,
    719      aEvent,
    720      this._view
    721    );
    722  },
    723 
    724  /**
    725   * Shows the Add Bookmark UI for the current insertion point.
    726   *
    727   * @param {string} aType
    728   *        the type of the new item (bookmark/folder)
    729   */
    730  async newItem(aType) {
    731    let ip = this._view.insertionPoint;
    732    if (!ip) {
    733      throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
    734    }
    735 
    736    let bookmarkGuid = await PlacesUIUtils.showBookmarkDialog(
    737      {
    738        action: "add",
    739        type: aType,
    740        defaultInsertionPoint: ip,
    741        hiddenRows: ["folderPicker"],
    742      },
    743      window.top
    744    );
    745    if (bookmarkGuid) {
    746      this._view.selectItems([bookmarkGuid], false);
    747    }
    748  },
    749 
    750  /**
    751   * Create a new Bookmark separator somewhere.
    752   */
    753  async newSeparator() {
    754    var ip = this._view.insertionPoint;
    755    if (!ip) {
    756      throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
    757    }
    758 
    759    let index = await ip.getIndex();
    760    let txn = PlacesTransactions.NewSeparator({ parentGuid: ip.guid, index });
    761    let guid = await txn.transact();
    762    // Select the new item.
    763    this._view.selectItems([guid], false);
    764  },
    765 
    766  /**
    767   * Sort the selected folder by name
    768   */
    769  async sortFolderByName() {
    770    let guid = PlacesUtils.getConcreteItemGuid(this._view.selectedNode);
    771    await PlacesTransactions.SortByName(guid).transact();
    772  },
    773 
    774  /**
    775   * Walk the list of folders we're removing in this delete operation, and
    776   * see if the selected node specified is already implicitly being removed
    777   * because it is a child of that folder.
    778   *
    779   * @param {object} node
    780   *        Node to check for containment.
    781   * @param {Array} pastFolders
    782   *        List of folders the calling function has already traversed
    783   * @returns {boolean} true if the node should be skipped, false otherwise.
    784   */
    785  _shouldSkipNode: function PC_shouldSkipNode(node, pastFolders) {
    786    /**
    787     * Determines if a node is contained by another node within a resultset.
    788     *
    789     * @param {object} parent
    790     *        The parent container to check for containment in
    791     * @returns {boolean} true if node is a member of parent's children, false otherwise.
    792     */
    793    function isNodeContainedBy(parent) {
    794      var cursor = node.parent;
    795      while (cursor) {
    796        if (cursor == parent) {
    797          return true;
    798        }
    799        cursor = cursor.parent;
    800      }
    801      return false;
    802    }
    803 
    804    for (var j = 0; j < pastFolders.length; ++j) {
    805      if (isNodeContainedBy(pastFolders[j])) {
    806        return true;
    807      }
    808    }
    809    return false;
    810  },
    811 
    812  /**
    813   * Creates a set of transactions for the removal of a range of items.
    814   * A range is an array of adjacent nodes in a view.
    815   *
    816   * @param {Array} range
    817   *          An array of nodes to remove. Should all be adjacent.
    818   * @param {Array} transactions
    819   *          An array of transactions (returned)
    820   * @param  {Array} [removedFolders]
    821   *          An array of folder nodes that have already been removed.
    822   * @returns {number} The total number of items affected.
    823   */
    824  async _removeRange(range, transactions, removedFolders) {
    825    if (!(transactions instanceof Array)) {
    826      throw new Error("Must pass a transactions array");
    827    }
    828    if (!removedFolders) {
    829      removedFolders = [];
    830    }
    831 
    832    let bmGuidsToRemove = [];
    833    let totalItems = 0;
    834 
    835    for (var i = 0; i < range.length; ++i) {
    836      var node = range[i];
    837      if (this._shouldSkipNode(node, removedFolders)) {
    838        continue;
    839      }
    840 
    841      totalItems++;
    842 
    843      if (PlacesUtils.nodeIsTagQuery(node.parent)) {
    844        // This is a uri node inside a tag container.  It needs a special
    845        // untag transaction.
    846        let tag = node.parent.title || "";
    847        if (!tag) {
    848          // The parent may be the root node, that doesn't have a title.
    849          tag = node.parent.query.tags[0];
    850        }
    851        transactions.push(PlacesTransactions.Untag({ urls: [node.uri], tag }));
    852      } else if (
    853        PlacesUtils.nodeIsTagQuery(node) &&
    854        node.parent &&
    855        PlacesUtils.nodeIsQuery(node.parent) &&
    856        PlacesUtils.asQuery(node.parent).queryOptions.resultType ==
    857          Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAGS_ROOT
    858      ) {
    859        // This is a tag container.
    860        // Untag all URIs tagged with this tag only if the tag container is
    861        // child of the "Tags" query in the library, in all other places we
    862        // must only remove the query node.
    863        let tag = node.title;
    864        let urls = new Set();
    865        await PlacesUtils.bookmarks.fetch({ tags: [tag] }, b =>
    866          urls.add(b.url)
    867        );
    868        transactions.push(
    869          PlacesTransactions.Untag({ tag, urls: Array.from(urls) })
    870        );
    871      } else if (
    872        PlacesUtils.nodeIsURI(node) &&
    873        PlacesUtils.nodeIsQuery(node.parent) &&
    874        PlacesUtils.asQuery(node.parent).queryOptions.queryType ==
    875          Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY
    876      ) {
    877        // This is a uri node inside an history query.
    878        await PlacesUtils.history.remove(node.uri).catch(console.error);
    879        // History deletes are not undoable, so we don't have a transaction.
    880      } else if (
    881        node.itemId == -1 &&
    882        PlacesUtils.nodeIsQuery(node) &&
    883        PlacesUtils.asQuery(node).queryOptions.queryType ==
    884          Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY
    885      ) {
    886        // This is a dynamically generated history query, like queries
    887        // grouped by site, time or both.  Dynamically generated queries don't
    888        // have an itemId even if they are descendants of a bookmark.
    889        await this._removeHistoryContainer(node).catch(console.error);
    890        // History deletes are not undoable, so we don't have a transaction.
    891      } else {
    892        // This is a common bookmark item.
    893        if (PlacesUtils.nodeIsFolderOrShortcut(node)) {
    894          // If this is a folder we add it to our array of folders, used
    895          // to skip nodes that are children of an already removed folder.
    896          removedFolders.push(node);
    897        }
    898        bmGuidsToRemove.push(node.bookmarkGuid);
    899      }
    900    }
    901    if (bmGuidsToRemove.length) {
    902      transactions.push(PlacesTransactions.Remove({ guids: bmGuidsToRemove }));
    903    }
    904    return totalItems;
    905  },
    906 
    907  async _removeRowsFromBookmarks() {
    908    let ranges = this._view.removableSelectionRanges;
    909    let transactions = [];
    910    let removedFolders = [];
    911    let totalItems = 0;
    912 
    913    for (let range of ranges) {
    914      totalItems += await this._removeRange(
    915        range,
    916        transactions,
    917        removedFolders
    918      );
    919    }
    920 
    921    if (transactions.length) {
    922      await PlacesUIUtils.batchUpdatesForNode(
    923        this._view.result,
    924        totalItems,
    925        async () => {
    926          await PlacesTransactions.batch(
    927            transactions,
    928            "PlacesController::removeRowsFromBookmarks"
    929          );
    930        }
    931      );
    932    }
    933  },
    934 
    935  /**
    936   * Removes the set of selected ranges from history, asynchronously. History
    937   * deletes are not undoable.
    938   */
    939  async _removeRowsFromHistory() {
    940    let nodes = this._view.selectedNodes;
    941    let URIs = new Set();
    942    for (let i = 0; i < nodes.length; ++i) {
    943      let node = nodes[i];
    944      if (PlacesUtils.nodeIsURI(node)) {
    945        URIs.add(node.uri);
    946      } else if (
    947        PlacesUtils.nodeIsQuery(node) &&
    948        PlacesUtils.asQuery(node).queryOptions.queryType ==
    949          Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY
    950      ) {
    951        await this._removeHistoryContainer(node).catch(console.error);
    952      }
    953    }
    954 
    955    if (URIs.size) {
    956      await PlacesUIUtils.batchUpdatesForNode(
    957        this._view.result,
    958        URIs.size,
    959        async () => {
    960          await PlacesUtils.history.remove([...URIs]);
    961        }
    962      );
    963    }
    964  },
    965 
    966  /**
    967   * Removes history visits for an history container node. History deletes are
    968   * not undoable.
    969   *
    970   * @param {object} aContainerNode
    971   *        The container node to remove.
    972   */
    973  async _removeHistoryContainer(aContainerNode) {
    974    if (PlacesUtils.nodeIsHost(aContainerNode)) {
    975      // This is a site container.
    976      // Check if it's the container for local files (don't be fooled by the
    977      // bogus string name, this is "(local files)").
    978      let host =
    979        "." +
    980        (aContainerNode.title == PlacesUtils.getString("localhost")
    981          ? ""
    982          : aContainerNode.title);
    983      // Will update faster if all children hidden before removing
    984      aContainerNode.containerOpen = false;
    985      await PlacesUtils.history.removeByFilter({ host });
    986    } else if (PlacesUtils.nodeIsDay(aContainerNode)) {
    987      // This is a day container.
    988      let query = aContainerNode.query;
    989      let beginTime = query.beginTime;
    990      let endTime = query.endTime;
    991      if (!query || !beginTime || !endTime) {
    992        throw new Error("A valid date container query should exist!");
    993      }
    994      // Will update faster if all children hidden before removing
    995      aContainerNode.containerOpen = false;
    996      // We want to exclude beginTime from the removal because
    997      // removePagesByTimeframe includes both extremes, while date containers
    998      // exclude the lower extreme.  So, if we would not exclude it, we would
    999      // end up removing more history than requested.
   1000      await PlacesUtils.history.removeByFilter({
   1001        beginDate: PlacesUtils.toDate(beginTime + 1000),
   1002        endDate: PlacesUtils.toDate(endTime),
   1003      });
   1004    }
   1005  },
   1006 
   1007  /**
   1008   * Removes the selection
   1009   */
   1010  async remove() {
   1011    if (!this._hasRemovableSelection()) {
   1012      return;
   1013    }
   1014 
   1015    // Sometimes we get repeated remove operation requests, because the user is
   1016    // holding down the DEL key. Since removal operations are asynchronous
   1017    // that would cause duplicated remove transactions that perform badly,
   1018    // increase memory usage (duplicate data), and cause failures (trying to
   1019    // act on already removed nodes).
   1020    if (this._isRepeatedRemoveOperation()) {
   1021      return;
   1022    }
   1023 
   1024    var root = this._view.result.root;
   1025 
   1026    if (PlacesUtils.nodeIsFolderOrShortcut(root)) {
   1027      await this._removeRowsFromBookmarks();
   1028    } else if (PlacesUtils.nodeIsQuery(root)) {
   1029      var queryType = PlacesUtils.asQuery(root).queryOptions.queryType;
   1030      if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS) {
   1031        await this._removeRowsFromBookmarks();
   1032      } else if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
   1033        await this._removeRowsFromHistory();
   1034      } else {
   1035        throw new Error("Unknown query type");
   1036      }
   1037    } else {
   1038      throw new Error("unexpected root");
   1039    }
   1040  },
   1041 
   1042  /**
   1043   * Fills a DataTransfer object with the content of the selection that can be
   1044   * dropped elsewhere.
   1045   *
   1046   * @param {object} aEvent
   1047   *        The dragstart event.
   1048   */
   1049  setDataTransfer: function PC_setDataTransfer(aEvent) {
   1050    let dt = aEvent.dataTransfer;
   1051 
   1052    let result = this._view.result;
   1053    let didSuppressNotifications = result.suppressNotifications;
   1054    if (!didSuppressNotifications) {
   1055      result.suppressNotifications = true;
   1056    }
   1057 
   1058    function addData(type, index) {
   1059      let wrapNode = PlacesUtils.wrapNode(node, type);
   1060      dt.mozSetDataAt(type, wrapNode, index);
   1061    }
   1062 
   1063    function addURIData(index) {
   1064      addData(PlacesUtils.TYPE_X_MOZ_URL, index);
   1065      addData(PlacesUtils.TYPE_PLAINTEXT, index);
   1066      addData(PlacesUtils.TYPE_HTML, index);
   1067    }
   1068 
   1069    try {
   1070      let nodes = this._view.draggableSelection;
   1071      for (let i = 0; i < nodes.length; ++i) {
   1072        var node = nodes[i];
   1073 
   1074        // This order is _important_! It controls how this and other
   1075        // applications select data to be inserted based on type.
   1076        addData(PlacesUtils.TYPE_X_MOZ_PLACE, i);
   1077        if (node.uri) {
   1078          addURIData(i);
   1079        }
   1080      }
   1081    } finally {
   1082      if (!didSuppressNotifications) {
   1083        result.suppressNotifications = false;
   1084      }
   1085    }
   1086  },
   1087 
   1088  get clipboardAction() {
   1089    let action = {};
   1090    let actionOwner;
   1091    try {
   1092      let xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(
   1093        Ci.nsITransferable
   1094      );
   1095      xferable.init(null);
   1096      xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION);
   1097      Services.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
   1098      xferable.getTransferData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, action);
   1099      [action, actionOwner] = action.value
   1100        .QueryInterface(Ci.nsISupportsString)
   1101        .data.split(",");
   1102    } catch (ex) {
   1103      // Paste from external sources don't have any associated action, just
   1104      // fallback to a copy action.
   1105      return "copy";
   1106    }
   1107    // For cuts also check who inited the action, since cuts across different
   1108    // instances should instead be handled as copies (The sources are not
   1109    // available for this instance).
   1110    if (action == "cut" && actionOwner != this.profileName) {
   1111      action = "copy";
   1112    }
   1113 
   1114    return action;
   1115  },
   1116 
   1117  _releaseClipboardOwnership: function PC__releaseClipboardOwnership() {
   1118    if (this.cutNodes.length) {
   1119      // This clears the logical clipboard, doesn't remove data.
   1120      Services.clipboard.emptyClipboard(Ci.nsIClipboard.kGlobalClipboard);
   1121    }
   1122  },
   1123 
   1124  _clearClipboard: function PC__clearClipboard() {
   1125    let xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(
   1126      Ci.nsITransferable
   1127    );
   1128    xferable.init(null);
   1129    // Empty transferables may cause crashes, so just add an unknown type.
   1130    const TYPE = "text/x-moz-place-empty";
   1131    xferable.addDataFlavor(TYPE);
   1132    xferable.setTransferData(TYPE, PlacesUtils.toISupportsString(""));
   1133    Services.clipboard.setData(
   1134      xferable,
   1135      null,
   1136      Ci.nsIClipboard.kGlobalClipboard
   1137    );
   1138  },
   1139 
   1140  _populateClipboard: function PC__populateClipboard(aNodes, aAction) {
   1141    // This order is _important_! It controls how this and other applications
   1142    // select data to be inserted based on type.
   1143    let contents = [
   1144      { type: PlacesUtils.TYPE_X_MOZ_PLACE, entries: [] },
   1145      { type: PlacesUtils.TYPE_X_MOZ_URL, entries: [] },
   1146      { type: PlacesUtils.TYPE_HTML, entries: [] },
   1147      { type: PlacesUtils.TYPE_PLAINTEXT, entries: [] },
   1148    ];
   1149 
   1150    // Avoid handling descendants of a copied node, the transactions take care
   1151    // of them automatically.
   1152    let copiedFolders = [];
   1153    aNodes.forEach(function (node) {
   1154      if (this._shouldSkipNode(node, copiedFolders)) {
   1155        return;
   1156      }
   1157      if (PlacesUtils.nodeIsFolderOrShortcut(node)) {
   1158        copiedFolders.push(node);
   1159      }
   1160 
   1161      contents.forEach(function (content) {
   1162        content.entries.push(PlacesUtils.wrapNode(node, content.type));
   1163      });
   1164    }, this);
   1165 
   1166    function addData(type, data) {
   1167      xferable.addDataFlavor(type);
   1168      xferable.setTransferData(type, PlacesUtils.toISupportsString(data));
   1169    }
   1170 
   1171    let xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(
   1172      Ci.nsITransferable
   1173    );
   1174    xferable.init(null);
   1175    let hasData = false;
   1176    // This order matters here!  It controls how this and other applications
   1177    // select data to be inserted based on type.
   1178    contents.forEach(function (content) {
   1179      if (content.entries.length) {
   1180        hasData = true;
   1181        let glue =
   1182          content.type == PlacesUtils.TYPE_X_MOZ_PLACE ? "," : PlacesUtils.endl;
   1183        addData(content.type, content.entries.join(glue));
   1184      }
   1185    });
   1186 
   1187    // Track the exected action in the xferable.  This must be the last flavor
   1188    // since it's the least preferred one.
   1189    // Enqueue a unique instance identifier to distinguish operations across
   1190    // concurrent instances of the application.
   1191    addData(
   1192      PlacesUtils.TYPE_X_MOZ_PLACE_ACTION,
   1193      aAction + "," + this.profileName
   1194    );
   1195 
   1196    if (hasData) {
   1197      Services.clipboard.setData(
   1198        xferable,
   1199        aAction == "cut" ? this : null,
   1200        Ci.nsIClipboard.kGlobalClipboard
   1201      );
   1202    }
   1203  },
   1204 
   1205  _cutNodes: [],
   1206  get cutNodes() {
   1207    return this._cutNodes;
   1208  },
   1209  set cutNodes(aNodes) {
   1210    let self = this;
   1211    function updateCutNodes(aValue) {
   1212      self._cutNodes.forEach(function (aNode) {
   1213        self._view.toggleCutNode(aNode, aValue);
   1214      });
   1215    }
   1216 
   1217    updateCutNodes(false);
   1218    this._cutNodes = aNodes;
   1219    updateCutNodes(true);
   1220  },
   1221 
   1222  /**
   1223   * Copy Bookmarks and Folders to the clipboard
   1224   */
   1225  copy: function PC_copy() {
   1226    let result = this._view.result;
   1227    let didSuppressNotifications = result.suppressNotifications;
   1228    if (!didSuppressNotifications) {
   1229      result.suppressNotifications = true;
   1230    }
   1231    try {
   1232      this._populateClipboard(this._view.selectedNodes, "copy");
   1233    } finally {
   1234      if (!didSuppressNotifications) {
   1235        result.suppressNotifications = false;
   1236      }
   1237    }
   1238  },
   1239 
   1240  /**
   1241   * Cut Bookmarks and Folders to the clipboard
   1242   */
   1243  cut: function PC_cut() {
   1244    let result = this._view.result;
   1245    let didSuppressNotifications = result.suppressNotifications;
   1246    if (!didSuppressNotifications) {
   1247      result.suppressNotifications = true;
   1248    }
   1249    try {
   1250      this._populateClipboard(this._view.selectedNodes, "cut");
   1251      this.cutNodes = this._view.selectedNodes;
   1252    } finally {
   1253      if (!didSuppressNotifications) {
   1254        result.suppressNotifications = false;
   1255      }
   1256    }
   1257  },
   1258 
   1259  /**
   1260   * Paste Bookmarks and Folders from the clipboard
   1261   */
   1262  async paste() {
   1263    // No reason to proceed if there isn't a valid insertion point.
   1264    let ip = this._view.insertionPoint;
   1265    if (!ip) {
   1266      throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
   1267    }
   1268 
   1269    let action = this.clipboardAction;
   1270 
   1271    let xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(
   1272      Ci.nsITransferable
   1273    );
   1274    xferable.init(null);
   1275    // This order matters here!  It controls the preferred flavors for this
   1276    // paste operation.
   1277    [
   1278      PlacesUtils.TYPE_X_MOZ_PLACE,
   1279      PlacesUtils.TYPE_X_MOZ_URL,
   1280      "application/x-torbrowser-opaque",
   1281      PlacesUtils.TYPE_PLAINTEXT,
   1282    ].forEach(type => xferable.addDataFlavor(type));
   1283 
   1284    Services.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
   1285 
   1286    // Now get the clipboard contents, in the best available flavor.
   1287    let validNodes, invalidNodes;
   1288 
   1289    try {
   1290      let data = {},
   1291        type = {};
   1292      xferable.getAnyTransferData(type, data);
   1293      ({ validNodes, invalidNodes } = PlacesUtils.unwrapNodes(
   1294        data.value.QueryInterface(Ci.nsISupportsString).data,
   1295        type.value
   1296      ));
   1297    } catch (ex) {
   1298      // No supported data exists, just bail out.
   1299      return;
   1300    }
   1301 
   1302    let doCopy = action == "copy";
   1303    let itemsToSelect = await PlacesUIUtils.handleTransferItems(
   1304      validNodes,
   1305      ip,
   1306      doCopy,
   1307      this._view
   1308    );
   1309 
   1310    // Cut/past operations are not repeatable, so clear the clipboard.
   1311    if (action == "cut") {
   1312      this._clearClipboard();
   1313    }
   1314 
   1315    if (itemsToSelect.length) {
   1316      this._view.selectItems(itemsToSelect, false);
   1317    }
   1318 
   1319    if (invalidNodes.length) {
   1320      let [title, body] = PlacesUIUtils.promptLocalization.formatValuesSync([
   1321        "places-bookmarks-paste-error-title",
   1322        "places-bookmarks-paste-error-message-header",
   1323      ]);
   1324 
   1325      const MAX_URI_LENGTH = 100;
   1326      const MAX_URI_COUNT = 20;
   1327 
   1328      let invalidUrlList = invalidNodes
   1329        .slice(0, MAX_URI_COUNT)
   1330        .map(item => {
   1331          let encodedUri = encodeURI(item.uri);
   1332          if (encodedUri.length > MAX_URI_LENGTH) {
   1333            encodedUri = encodedUri.slice(0, MAX_URI_LENGTH) + "…";
   1334          }
   1335          return "\n  • " + encodedUri;
   1336        })
   1337        .join("");
   1338 
   1339      if (invalidNodes.length > MAX_URI_COUNT) {
   1340        invalidUrlList += "\n  • …";
   1341      }
   1342 
   1343      body = `${body}${invalidUrlList}`;
   1344      Services.prompt.alert(window, title, body);
   1345    }
   1346  },
   1347 
   1348  /**
   1349   * Checks if we can insert into a container.
   1350   *
   1351   * @param {object} container
   1352   *          The container were we are want to drop
   1353   * @returns {boolean}
   1354   */
   1355  disallowInsertion(container) {
   1356    if (!container) {
   1357      throw new Error("empty container");
   1358    }
   1359    // Allow dropping into Tag containers and editable folders.
   1360    return (
   1361      !PlacesUtils.nodeIsTagQuery(container) &&
   1362      (!PlacesUtils.nodeIsFolderOrShortcut(container) ||
   1363        PlacesUIUtils.isFolderReadOnly(container))
   1364    );
   1365  },
   1366 
   1367  /**
   1368   * Determines if a node can be moved.
   1369   *
   1370   * @param {object} node
   1371   *        A nsINavHistoryResultNode node.
   1372   * @returns {boolean} True if the node can be moved, false otherwise.
   1373   */
   1374  canMoveNode(node) {
   1375    // Only bookmark items are movable.
   1376    if (node.itemId == -1) {
   1377      return false;
   1378    }
   1379 
   1380    // Once tags and bookmarked are divorced, the tag-query check should be
   1381    // removed.
   1382    let parentNode = node.parent;
   1383    if (!parentNode) {
   1384      return false;
   1385    }
   1386 
   1387    // Once tags and bookmarked are divorced, the tag-query check should be
   1388    // removed.
   1389    if (PlacesUtils.nodeIsTagQuery(parentNode)) {
   1390      return false;
   1391    }
   1392 
   1393    return (
   1394      (PlacesUtils.nodeIsFolderOrShortcut(parentNode) &&
   1395        !PlacesUIUtils.isFolderReadOnly(parentNode)) ||
   1396      PlacesUtils.nodeIsQuery(parentNode)
   1397    );
   1398  },
   1399  async forgetAboutThisSite() {
   1400    let host;
   1401    if (PlacesUtils.nodeIsHost(this._view.selectedNode)) {
   1402      host = this._view.selectedNode.query.domain;
   1403    } else {
   1404      host = Services.io.newURI(this._view.selectedNode.uri).host;
   1405    }
   1406    let baseDomain;
   1407    try {
   1408      baseDomain = Services.eTLD.getBaseDomainFromHost(host);
   1409    } catch (e) {
   1410      // If there is no baseDomain we fall back to host
   1411    }
   1412    let params = { host, hostOrBaseDomain: baseDomain ?? host };
   1413    if (window.gDialogBox) {
   1414      await window.gDialogBox.open(
   1415        "chrome://browser/content/places/clearDataForSite.xhtml",
   1416        params
   1417      );
   1418    } else {
   1419      await window.openDialog(
   1420        "chrome://browser/content/places/clearDataForSite.xhtml",
   1421        null,
   1422        "modal,centerscreen",
   1423        params
   1424      );
   1425    }
   1426  },
   1427 
   1428  showInFolder(aBookmarkGuid) {
   1429    // Open containing folder in left pane/sidebar bookmark tree
   1430    let documentUrl = document.documentURI.toLowerCase();
   1431    if (documentUrl.endsWith("browser.xhtml")) {
   1432      // We're in a menu or a panel.
   1433      window.SidebarController._show("viewBookmarksSidebar").then(() => {
   1434        let theSidebar = document.getElementById("sidebar");
   1435        theSidebar.contentDocument
   1436          .getElementById("bookmarks-view")
   1437          .selectItems([aBookmarkGuid]);
   1438      });
   1439    } else if (documentUrl.includes("sidebar")) {
   1440      // We're in the sidebar - clear the search box first
   1441      let searchBox = document.getElementById("search-box");
   1442      searchBox.clear();
   1443 
   1444      // And go to the node
   1445      this._view.selectItems([aBookmarkGuid], true);
   1446    } else {
   1447      // We're in the bookmark library/manager
   1448      PlacesUtils.bookmarks
   1449        .fetch(aBookmarkGuid, null, { includePath: true })
   1450        .then(b => {
   1451          let containers = b.path.map(obj => {
   1452            return obj.guid;
   1453          });
   1454          // selectLeftPane looks for literal "AllBookmarks" as a "built-in"
   1455          containers.splice(0, 0, "AllBookmarks");
   1456          PlacesOrganizer.selectLeftPaneContainerByHierarchy(containers);
   1457          this._view.selectItems([aBookmarkGuid], false);
   1458        });
   1459    }
   1460  },
   1461 };
   1462 
   1463 /**
   1464 * Handles drag and drop operations for views. Note that this is view agnostic!
   1465 * You should not use PlacesController._view within these methods, since
   1466 * the view that the item(s) have been dropped on was not necessarily active.
   1467 * Drop functions are passed the view that is being dropped on.
   1468 */
   1469 var PlacesControllerDragHelper = {
   1470  /**
   1471   * For views using DOM nodes like toolbars, menus and panels, this is the DOM
   1472   * element currently being dragged over. For other views not handling DOM
   1473   * nodes, like trees, it is a Places result node instead.
   1474   */
   1475  currentDropTarget: null,
   1476 
   1477  /**
   1478   * Determines if the mouse is currently being dragged over a child node of
   1479   * this menu. This is necessary so that the menu doesn't close while the
   1480   * mouse is dragging over one of its submenus
   1481   *
   1482   * @param {object} node
   1483   *        The container node
   1484   * @returns {boolean} true if the user is dragging over a node within the hierarchy of
   1485   *         the container, false otherwise.
   1486   */
   1487  draggingOverChildNode: function PCDH_draggingOverChildNode(node) {
   1488    let currentNode = this.currentDropTarget;
   1489    while (currentNode) {
   1490      if (currentNode == node) {
   1491        return true;
   1492      }
   1493      currentNode = currentNode.parentNode;
   1494    }
   1495    return false;
   1496  },
   1497 
   1498  /**
   1499   * @returns {object|null} The current active drag session for the window.
   1500   * Returns null if there is none.
   1501   */
   1502  getSession: function PCDH__getSession() {
   1503    return this.dragService.getCurrentSession(window);
   1504  },
   1505 
   1506  /**
   1507   * Extract the most relevant flavor from a list of flavors.
   1508   *
   1509   * @param {DOMStringList} flavors The flavors list.
   1510   * @returns {string} The most relevant flavor, or undefined.
   1511   */
   1512  getMostRelevantFlavor(flavors) {
   1513    // The DnD API returns a DOMStringList, but tests may pass an Array.
   1514    flavors = Array.from(flavors);
   1515    return PlacesUIUtils.SUPPORTED_FLAVORS.find(f => flavors.includes(f));
   1516  },
   1517 
   1518  /**
   1519   * Determines whether or not the data currently being dragged can be dropped
   1520   * on a places view.
   1521   *
   1522   * @param {object} ip
   1523   *        The insertion point where the items should be dropped.
   1524   * @param {object} dt
   1525   *        The data transfer object.
   1526   * @returns {boolean}
   1527   */
   1528  canDrop: function PCDH_canDrop(ip, dt) {
   1529    let dropCount = dt.mozItemCount;
   1530 
   1531    // Check every dragged item.
   1532    for (let i = 0; i < dropCount; i++) {
   1533      let flavor = this.getMostRelevantFlavor(dt.mozTypesAt(i));
   1534      if (!flavor) {
   1535        return false;
   1536      }
   1537 
   1538      // Urls can be dropped on any insertionpoint.
   1539      // XXXmano: remember that this method is called for each dragover event!
   1540      // Thus we shouldn't use unwrapNodes here at all if possible.
   1541      // I think it would be OK to accept bogus data here (e.g. text which was
   1542      // somehow wrapped as TAB_DROP_TYPE, this is not in our control, and
   1543      // will just case the actual drop to be a no-op), and only rule out valid
   1544      // expected cases, which are either unsupported flavors, or items which
   1545      // cannot be dropped in the current insertionpoint. The last case will
   1546      // likely force us to use unwrapNodes for the private data types of
   1547      // places.
   1548      if (flavor == TAB_DROP_TYPE) {
   1549        continue;
   1550      }
   1551 
   1552      let data = dt.mozGetDataAt(flavor, i);
   1553      let validNodes;
   1554      try {
   1555        ({ validNodes } = PlacesUtils.unwrapNodes(data, flavor));
   1556      } catch (e) {
   1557        return false;
   1558      }
   1559 
   1560      for (let dragged of validNodes) {
   1561        // Only bookmarks and urls can be dropped into tag containers.
   1562        if (
   1563          ip.isTag &&
   1564          dragged.type != PlacesUtils.TYPE_X_MOZ_URL &&
   1565          (dragged.type != PlacesUtils.TYPE_X_MOZ_PLACE ||
   1566            (dragged.uri && dragged.uri.startsWith("place:")))
   1567        ) {
   1568          return false;
   1569        }
   1570 
   1571        // Disallow dropping of a folder on itself or any of its descendants.
   1572        // This check is done to show an appropriate drop indicator, a stricter
   1573        // check is done later by the bookmarks API.
   1574        if (
   1575          dragged.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER ||
   1576          (dragged.uri && dragged.uri.startsWith("place:"))
   1577        ) {
   1578          let dragOverPlacesNode = this.currentDropTarget;
   1579          if (!(dragOverPlacesNode instanceof Ci.nsINavHistoryResultNode)) {
   1580            // If it's a DOM node, it should have a _placesNode expando, or it
   1581            // may be a static element in a places container, like the [empty]
   1582            // menuitem.
   1583            dragOverPlacesNode =
   1584              dragOverPlacesNode._placesNode ??
   1585              dragOverPlacesNode.parentNode?._placesNode;
   1586          }
   1587 
   1588          // If we couldn't get a target Places result node then we can't check
   1589          // whether the drag is allowed, just let it go through.
   1590          if (dragOverPlacesNode) {
   1591            let guid = dragged.concreteGuid ?? dragged.itemGuid;
   1592            // Dragging over itself.
   1593            if (PlacesUtils.getConcreteItemGuid(dragOverPlacesNode) == guid) {
   1594              return false;
   1595            }
   1596            // Dragging over a descendant.
   1597            for (let ancestor of PlacesUtils.nodeAncestors(
   1598              dragOverPlacesNode
   1599            )) {
   1600              if (PlacesUtils.getConcreteItemGuid(ancestor) == guid) {
   1601                return false;
   1602              }
   1603            }
   1604          }
   1605        }
   1606 
   1607        // Disallow the dropping of multiple bookmarks if they include
   1608        // a javascript: bookmarklet
   1609        if (
   1610          !flavor.startsWith("text/x-moz-place") &&
   1611          (validNodes.length > 1 || dropCount > 1) &&
   1612          validNodes.some(n => n.uri?.startsWith("javascript:"))
   1613        ) {
   1614          return false;
   1615        }
   1616      }
   1617    }
   1618    return true;
   1619  },
   1620 
   1621  /**
   1622   * Handles the drop of one or more items onto a view.
   1623   *
   1624   * @param {object} insertionPoint The insertion point where the items should
   1625   *                                be dropped.
   1626   * @param {object} dt             The dataTransfer information for the drop.
   1627   * @param {object} [view]         The view or the tree element. This allows
   1628   *                                batching to take place.
   1629   */
   1630  async onDrop(insertionPoint, dt, view) {
   1631    let doCopy = ["copy", "link"].includes(dt.dropEffect);
   1632 
   1633    let dropCount = dt.mozItemCount;
   1634 
   1635    // Following flavors may contain duplicated data.
   1636    let duplicable = new Map();
   1637    duplicable.set(PlacesUtils.TYPE_PLAINTEXT, new Set());
   1638    duplicable.set(PlacesUtils.TYPE_X_MOZ_URL, new Set());
   1639 
   1640    // Collect all data from the DataTransfer before processing it, as the
   1641    // DataTransfer is only valid during the synchronous handling of the `drop`
   1642    // event handler callback.
   1643    let nodes = [];
   1644    let externalDrag = false;
   1645    for (let i = 0; i < dropCount; ++i) {
   1646      let flavor = this.getMostRelevantFlavor(dt.mozTypesAt(i));
   1647      if (!flavor) {
   1648        return;
   1649      }
   1650 
   1651      let data = dt.mozGetDataAt(flavor, i);
   1652      if (duplicable.has(flavor)) {
   1653        let handled = duplicable.get(flavor);
   1654        if (handled.has(data)) {
   1655          continue;
   1656        }
   1657        handled.add(data);
   1658      }
   1659 
   1660      // Check that the drag/drop is not internal
   1661      if (i == 0 && !flavor.startsWith("text/x-moz-place")) {
   1662        externalDrag = true;
   1663      }
   1664 
   1665      if (flavor != TAB_DROP_TYPE) {
   1666        nodes = [...nodes, ...PlacesUtils.unwrapNodes(data, flavor).validNodes];
   1667      } else if (
   1668        XULElement.isInstance(data) &&
   1669        data.localName == "tab" &&
   1670        data.ownerGlobal.isChromeWindow
   1671      ) {
   1672        let uri = data.linkedBrowser.currentURI;
   1673        let spec = uri ? uri.spec : "about:blank";
   1674        nodes.push({
   1675          uri: spec,
   1676          title: data.label,
   1677          type: PlacesUtils.TYPE_X_MOZ_URL,
   1678        });
   1679      } else {
   1680        throw new Error("bogus data was passed as a tab");
   1681      }
   1682    }
   1683 
   1684    // If a multiple urls are being dropped from the urlbar or an external source,
   1685    // and they include javascript url, not bookmark any of them
   1686    if (
   1687      externalDrag &&
   1688      (nodes.length > 1 || dropCount > 1) &&
   1689      nodes.some(n => n.uri?.startsWith("javascript:"))
   1690    ) {
   1691      throw new Error("Javascript bookmarklet passed with uris");
   1692    }
   1693 
   1694    // If a single javascript url is being dropped from the urlbar or an external source,
   1695    // show the bookmark dialog as a speedbump protection against malicious cases.
   1696    if (
   1697      nodes.length == 1 &&
   1698      externalDrag &&
   1699      nodes[0].uri?.startsWith("javascript")
   1700    ) {
   1701      let uri;
   1702      try {
   1703        uri = Services.io.newURI(nodes[0].uri);
   1704      } catch (ex) {
   1705        // Invalid uri, we skip this code and the entry will be discarded later.
   1706      }
   1707 
   1708      if (uri) {
   1709        let bookmarkGuid = await PlacesUIUtils.showBookmarkDialog(
   1710          {
   1711            action: "add",
   1712            type: "bookmark",
   1713            defaultInsertionPoint: insertionPoint,
   1714            hiddenRows: ["folderPicker"],
   1715            title: nodes[0].title,
   1716            uri,
   1717          },
   1718          BrowserWindowTracker.getTopWindow() // `window` may be the Library.
   1719        );
   1720 
   1721        if (bookmarkGuid && view) {
   1722          view.selectItems([bookmarkGuid], false);
   1723        }
   1724 
   1725        return;
   1726      }
   1727    }
   1728 
   1729    await PlacesUIUtils.handleTransferItems(
   1730      nodes,
   1731      insertionPoint,
   1732      doCopy,
   1733      view
   1734    );
   1735  },
   1736 };
   1737 
   1738 XPCOMUtils.defineLazyServiceGetter(
   1739  PlacesControllerDragHelper,
   1740  "dragService",
   1741  "@mozilla.org/widget/dragservice;1",
   1742  Ci.nsIDragService
   1743 );