tor-browser

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

PlacesUIUtils.sys.mjs (64032B)


      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 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      7 
      8 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      9 
     10 const lazy = {};
     11 
     12 ChromeUtils.defineESModuleGetters(lazy, {
     13  CLIENT_NOT_CONFIGURED: "resource://services-sync/constants.sys.mjs",
     14  BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
     15  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
     16  CustomizableUI:
     17    "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
     18  MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs",
     19  OpenInTabsUtils:
     20    "moz-src:///browser/components/tabbrowser/OpenInTabsUtils.sys.mjs",
     21  PlacesTransactions: "resource://gre/modules/PlacesTransactions.sys.mjs",
     22  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     23  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     24  Weave: "resource://services-sync/main.sys.mjs",
     25 });
     26 
     27 const ITEM_CHANGED_BATCH_NOTIFICATION_THRESHOLD = 10;
     28 
     29 // copied from utilityOverlay.js
     30 const TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
     31 
     32 /**
     33 * Collects all information for a bookmark and performs editmethods
     34 */
     35 class BookmarkState {
     36  /**
     37   * Construct a new BookmarkState.
     38   *
     39   * @param {object} options
     40   *   The constructor options.
     41   * @param {object} options.info
     42   *   Either a result node or a node-like object representing the item to be edited.
     43   * @param {string} [options.tags]
     44   *   Tags (if any) for the bookmark in a comma separated string. Empty tags are
     45   *   skipped
     46   * @param {string} [options.keyword]
     47   *   Existing (if there are any) keyword for bookmark
     48   * @param {boolean} [options.isFolder]
     49   *   If the item is a folder.
     50   * @param {Array<{ title: string; url: nsIURI; }>} [options.children]
     51   *   The list of child URIs to bookmark within the folder.
     52   * @param {boolean} [options.autosave]
     53   *   If changes to bookmark fields should be saved immediately after calling
     54   *   its respective "changed" method, rather than waiting for save() to be
     55   *   called.
     56   * @param {number} [options.index]
     57   *   The insertion point index of the bookmark.
     58   */
     59  constructor({
     60    info,
     61    tags = "",
     62    keyword = "",
     63    isFolder = false,
     64    children = [],
     65    autosave = false,
     66    index,
     67  }) {
     68    this._guid = info.itemGuid;
     69    this._postData = info.postData;
     70    this._isTagContainer = info.isTag;
     71    this._bulkTaggingUrls = info.uris?.map(uri => uri.spec);
     72    this._isFolder = isFolder;
     73    this._children = children;
     74    this._autosave = autosave;
     75 
     76    // Original Bookmark
     77    this._originalState = {
     78      title: this._isTagContainer ? info.tag : info.title,
     79      uri: info.uri?.spec,
     80      tags: tags
     81        .trim()
     82        .split(/\s*,\s*/)
     83        .filter(tag => !!tag.length),
     84      keyword,
     85      parentGuid: info.parentGuid,
     86      index,
     87    };
     88 
     89    // Edited bookmark
     90    this._newState = {};
     91  }
     92 
     93  /**
     94   * Save edited title for the bookmark
     95   *
     96   * @param {string} title
     97   *   The title of the bookmark
     98   */
     99  async _titleChanged(title) {
    100    this._newState.title = title;
    101    await this._maybeSave();
    102  }
    103 
    104  /**
    105   * Save edited location for the bookmark
    106   *
    107   * @param {string} location
    108   *   The location of the bookmark
    109   */
    110  async _locationChanged(location) {
    111    this._newState.uri = location;
    112    await this._maybeSave();
    113  }
    114 
    115  /**
    116   * Save edited tags for the bookmark
    117   *
    118   * @param {string} tags
    119   *    Comma separated list of tags
    120   */
    121  async _tagsChanged(tags) {
    122    this._newState.tags = tags;
    123    await this._maybeSave();
    124  }
    125 
    126  /**
    127   * Save edited keyword for the bookmark
    128   *
    129   * @param {string} keyword
    130   *   The keyword of the bookmark
    131   */
    132  async _keywordChanged(keyword) {
    133    this._newState.keyword = keyword;
    134    await this._maybeSave();
    135  }
    136 
    137  /**
    138   * Save edited parentGuid for the bookmark
    139   *
    140   * @param {string} parentGuid
    141   *   The parentGuid of the bookmark
    142   */
    143  async _parentGuidChanged(parentGuid) {
    144    this._newState.parentGuid = parentGuid;
    145    await this._maybeSave();
    146  }
    147 
    148  /**
    149   * Save changes if autosave is enabled.
    150   */
    151  async _maybeSave() {
    152    if (this._autosave) {
    153      await this.save();
    154    }
    155  }
    156 
    157  /**
    158   * Create a new bookmark.
    159   *
    160   * @returns {Promise<string>} The bookmark's GUID.
    161   */
    162  async _createBookmark() {
    163    let transactions = [
    164      lazy.PlacesTransactions.NewBookmark({
    165        parentGuid: this.parentGuid,
    166        tags: this._newState.tags,
    167        title: this._newState.title ?? this._originalState.title,
    168        url: this._newState.uri ?? this._originalState.uri,
    169        index: this._originalState.index,
    170      }),
    171    ];
    172    if (this._newState.keyword) {
    173      transactions.push(previousResults =>
    174        lazy.PlacesTransactions.EditKeyword({
    175          guid: previousResults[0],
    176          keyword: this._newState.keyword,
    177          postData: this._postData,
    178        })
    179      );
    180    }
    181    let results = await lazy.PlacesTransactions.batch(
    182      transactions,
    183      "BookmarkState::createBookmark"
    184    );
    185    this._guid = results?.[0];
    186    return this._guid;
    187  }
    188 
    189  /**
    190   * Create a new folder.
    191   *
    192   * @returns {Promise<string>} The folder's GUID.
    193   */
    194  async _createFolder() {
    195    let transactions = [
    196      lazy.PlacesTransactions.NewFolder({
    197        parentGuid: this.parentGuid,
    198        title: this._newState.title ?? this._originalState.title,
    199        children: this._children,
    200        index: this._originalState.index,
    201        tags: this._newState.tags,
    202      }),
    203    ];
    204 
    205    if (this._bulkTaggingUrls) {
    206      this._appendTagsTransactions({
    207        transactions,
    208        newTags: this._newState.tags,
    209        originalTags: this._originalState.tags,
    210        urls: this._bulkTaggingUrls,
    211      });
    212    }
    213 
    214    let results = await lazy.PlacesTransactions.batch(
    215      transactions,
    216      "BookmarkState::save::createFolder"
    217    );
    218    this._guid = results[0];
    219    return this._guid;
    220  }
    221 
    222  get parentGuid() {
    223    return this._newState.parentGuid ?? this._originalState.parentGuid;
    224  }
    225 
    226  /**
    227   * Save() API function for bookmark.
    228   *
    229   * @returns {Promise<string>} bookmark.guid
    230   */
    231  async save() {
    232    if (this._guid === lazy.PlacesUtils.bookmarks.unsavedGuid) {
    233      return this._isFolder ? this._createFolder() : this._createBookmark();
    234    }
    235 
    236    if (!Object.keys(this._newState).length) {
    237      return this._guid;
    238    }
    239 
    240    if (this._isTagContainer && this._newState.title) {
    241      await lazy.PlacesTransactions.RenameTag({
    242        oldTag: this._originalState.title,
    243        tag: this._newState.title,
    244      })
    245        .transact()
    246        .catch(console.error);
    247      return this._guid;
    248    }
    249 
    250    let url = this._newState.uri || this._originalState.uri;
    251    let transactions = [];
    252 
    253    if (this._newState.uri) {
    254      transactions.push(
    255        lazy.PlacesTransactions.EditUrl({
    256          guid: this._guid,
    257          url,
    258        })
    259      );
    260    }
    261 
    262    for (const [key, value] of Object.entries(this._newState)) {
    263      switch (key) {
    264        case "title":
    265          transactions.push(
    266            lazy.PlacesTransactions.EditTitle({
    267              guid: this._guid,
    268              title: value,
    269            })
    270          );
    271          break;
    272        case "tags": {
    273          this._appendTagsTransactions({
    274            transactions,
    275            newTags: value,
    276            originalTags: this._originalState.tags,
    277            urls: this._bulkTaggingUrls || [url],
    278          });
    279          break;
    280        }
    281        case "keyword":
    282          transactions.push(
    283            lazy.PlacesTransactions.EditKeyword({
    284              guid: this._guid,
    285              keyword: value,
    286              postData: this._postData,
    287              oldKeyword: this._originalState.keyword,
    288            })
    289          );
    290          break;
    291        case "parentGuid":
    292          transactions.push(
    293            lazy.PlacesTransactions.Move({
    294              guid: this._guid,
    295              newParentGuid: this._newState.parentGuid,
    296            })
    297          );
    298          break;
    299      }
    300    }
    301    if (transactions.length) {
    302      await lazy.PlacesTransactions.batch(transactions, "BookmarkState::save");
    303    }
    304 
    305    this._originalState = { ...this._originalState, ...this._newState };
    306    this._newState = {};
    307    return this._guid;
    308  }
    309 
    310  /**
    311   * Append transactions to update tags by given information.
    312   *
    313   * @param {object} parameters
    314   *   The parameters object containing:
    315   * @param {object[]} parameters.transactions
    316   *   Array that transactions will be appended to.
    317   * @param {string[]} parameters.newTags
    318   *   Tags that will be appended to the given urls.
    319   * @param {string[]} parameters.originalTags
    320   *   Tags that had been appended to the given urls.
    321   * @param {string[]} parameters.urls
    322   *   URLs that will be updated.
    323   */
    324  _appendTagsTransactions({
    325    transactions,
    326    newTags = [],
    327    originalTags = [],
    328    urls,
    329  }) {
    330    const addedTags = newTags.filter(tag => !originalTags.includes(tag));
    331    const removedTags = originalTags.filter(tag => !newTags.includes(tag));
    332    if (addedTags.length) {
    333      transactions.push(
    334        lazy.PlacesTransactions.Tag({
    335          urls,
    336          tags: addedTags,
    337        })
    338      );
    339    }
    340    if (removedTags.length) {
    341      transactions.push(
    342        lazy.PlacesTransactions.Untag({
    343          urls,
    344          tags: removedTags,
    345        })
    346      );
    347    }
    348  }
    349 }
    350 
    351 export var PlacesUIUtils = {
    352  BookmarkState,
    353  _bookmarkToolbarTelemetryListening: false,
    354  LAST_USED_FOLDERS_META_KEY: "bookmarks/lastusedfolders",
    355 
    356  lastContextMenuTriggerNode: null,
    357 
    358  lastContextMenuCommand: null,
    359 
    360  // This allows to await for all the relevant bookmark changes to be applied
    361  // when a bookmark dialog is closed. It is resolved to the bookmark guid,
    362  // if a bookmark was created or modified.
    363  lastBookmarkDialogDeferred: null,
    364 
    365  /**
    366   * Obfuscates a place: URL to use it in xulstore without the risk of
    367   leaking browsing information. Uses md5 to hash the query string.
    368   *
    369   * @param {string} url
    370   *        the URL for xulstore with place: key pairs.
    371   * @returns {string} "place:[md5_hash]" hashed url
    372   */
    373 
    374  obfuscateUrlForXulStore(url) {
    375    if (!url.startsWith("place:")) {
    376      throw new Error("Method must be used to only obfuscate place: uris!");
    377    }
    378    let urlNoProtocol = url.substring(url.indexOf(":") + 1);
    379    let hashedURL = lazy.PlacesUtils.md5(urlNoProtocol);
    380 
    381    return `place:${hashedURL}`;
    382  },
    383 
    384  /**
    385   * Shows the bookmark dialog corresponding to the specified info.
    386   *
    387   * @param {object} aInfo
    388   *        Describes the item to be edited/added in the dialog.
    389   *        See documentation at the top of bookmarkProperties.js
    390   * @param {Window} [aParentWindow]
    391   *        Owner window for the new dialog.
    392   *
    393   * @see documentation at the top of bookmarkProperties.js
    394   * @returns {Promise<string>} The guid of the item that was created or edited,
    395   *                   undefined otherwise.
    396   */
    397  async showBookmarkDialog(aInfo, aParentWindow = null) {
    398    this.lastBookmarkDialogDeferred = Promise.withResolvers();
    399 
    400    let dialogURL = "chrome://browser/content/places/bookmarkProperties.xhtml";
    401    let features = "centerscreen,chrome,modal,resizable=no";
    402    let bookmarkGuid;
    403 
    404    if (!aParentWindow) {
    405      aParentWindow = Services.wm.getMostRecentWindow(null);
    406    }
    407 
    408    if (aParentWindow.gDialogBox) {
    409      await aParentWindow.gDialogBox.open(dialogURL, aInfo);
    410    } else {
    411      aParentWindow.openDialog(dialogURL, "", features, aInfo);
    412    }
    413 
    414    if (aInfo.bookmarkState) {
    415      bookmarkGuid = await aInfo.bookmarkState.save();
    416      this.lastBookmarkDialogDeferred.resolve(bookmarkGuid);
    417      return bookmarkGuid;
    418    }
    419    bookmarkGuid = undefined;
    420    this.lastBookmarkDialogDeferred.resolve(bookmarkGuid);
    421    return bookmarkGuid;
    422  },
    423 
    424  /**
    425   * Bookmarks one or more pages. If there is more than one, this will create
    426   * the bookmarks in a new folder.
    427   *
    428   * @param {{uri: nsIURI, title: string}[]} URIList
    429   *   The list of URIs to bookmark.
    430   * @param {string[]} [hiddenRows]
    431   *   An array of rows to be hidden.
    432   * @param {Window} [win]
    433   *   The window to use as the parent to display the bookmark dialog.
    434   */
    435  async showBookmarkPagesDialog(URIList, hiddenRows = [], win = null) {
    436    if (!URIList.length) {
    437      return;
    438    }
    439 
    440    const bookmarkDialogInfo = { action: "add", hiddenRows };
    441    if (URIList.length > 1) {
    442      bookmarkDialogInfo.type = "folder";
    443      bookmarkDialogInfo.URIList = URIList;
    444    } else {
    445      bookmarkDialogInfo.type = "bookmark";
    446      bookmarkDialogInfo.title = URIList[0].title;
    447      bookmarkDialogInfo.uri = URIList[0].uri;
    448    }
    449 
    450    await PlacesUIUtils.showBookmarkDialog(bookmarkDialogInfo, win);
    451  },
    452 
    453  /**
    454   * Returns the closet ancestor places view for the given DOM node
    455   *
    456   * @param {DOMNode} aNode
    457   *        a DOM node
    458   * @returns {DOMNode} the closest ancestor places view if exists, null otherwsie.
    459   */
    460  getViewForNode: function PUIU_getViewForNode(aNode) {
    461    let node = aNode;
    462 
    463    if (Cu.isDeadWrapper(node)) {
    464      return null;
    465    }
    466 
    467    if (node.localName == "panelview" && node._placesView) {
    468      return node._placesView;
    469    }
    470 
    471    // The view for a <menu> of which its associated menupopup is a places
    472    // view, is the menupopup.
    473    if (
    474      node.localName == "menu" &&
    475      !node._placesNode &&
    476      node.menupopup._placesView
    477    ) {
    478      return node.menupopup._placesView;
    479    }
    480 
    481    while (Element.isInstance(node)) {
    482      if (node._placesView) {
    483        return node._placesView;
    484      }
    485      if (
    486        node.localName == "tree" &&
    487        node.getAttribute("is") == "places-tree"
    488      ) {
    489        return node;
    490      }
    491 
    492      node = node.parentNode;
    493    }
    494 
    495    return null;
    496  },
    497 
    498  /**
    499   * Returns the active PlacesController for a given command.
    500   *
    501   * @param {Window} win The window containing the affected view
    502   * @param {string} command The command
    503   * @returns {PlacesController} a places controller
    504   */
    505  getControllerForCommand(win, command) {
    506    // If we're building a context menu for a non-focusable view, for example
    507    // a menupopup, we must return the view that triggered the context menu.
    508    let popupNode = PlacesUIUtils.lastContextMenuTriggerNode;
    509    if (popupNode) {
    510      let isManaged = !!popupNode.closest("#managed-bookmarks");
    511      if (isManaged) {
    512        return this.managedBookmarksController;
    513      }
    514      let view = this.getViewForNode(popupNode);
    515      if (view && view._contextMenuShown) {
    516        return view.controllers.getControllerForCommand(command);
    517      }
    518    }
    519 
    520    // When we're not building a context menu, only focusable views
    521    // are possible.  Thus, we can safely use the command dispatcher.
    522    let controller =
    523      win.top.document.commandDispatcher.getControllerForCommand(command);
    524    return controller || null;
    525  },
    526 
    527  /**
    528   * Update all the Places commands for the given window.
    529   *
    530   * @param {Window} win The window to update.
    531   */
    532  updateCommands(win) {
    533    // Get the controller for one of the places commands.
    534    let controller = this.getControllerForCommand(win, "placesCmd_open");
    535    for (let command of [
    536      "placesCmd_open",
    537      "placesCmd_open:window",
    538      "placesCmd_open:privatewindow",
    539      "placesCmd_open:tab",
    540      "placesCmd_new:folder",
    541      "placesCmd_new:bookmark",
    542      "placesCmd_new:separator",
    543      "placesCmd_show:info",
    544      "placesCmd_reload",
    545      "placesCmd_sortBy:name",
    546      "placesCmd_cut",
    547      "placesCmd_copy",
    548      "placesCmd_paste",
    549      "placesCmd_delete",
    550      "placesCmd_showInFolder",
    551    ]) {
    552      win.goSetCommandEnabled(
    553        command,
    554        controller && controller.isCommandEnabled(command)
    555      );
    556    }
    557  },
    558 
    559  /**
    560   * Executes the given command on the currently active controller.
    561   *
    562   * @param {Window} win The window containing the affected view
    563   * @param {string} command The command to execute
    564   */
    565  doCommand(win, command) {
    566    let controller = this.getControllerForCommand(win, command);
    567    if (controller && controller.isCommandEnabled(command)) {
    568      PlacesUIUtils.lastContextMenuCommand = command;
    569      controller.doCommand(command);
    570    }
    571  },
    572 
    573  /**
    574   * By calling this before visiting an URL, the visit will be associated to a
    575   * TRANSITION_TYPED transition (if there is no a referrer).
    576   * This is used when visiting pages from the history menu, history sidebar,
    577   * url bar, url autocomplete results, and history searches from the places
    578   * organizer.  If this is not called visits will be marked as
    579   * TRANSITION_LINK.
    580   *
    581   * @param {string} aURL
    582   *   The URL to mark as typed.
    583   */
    584  markPageAsTyped: function PUIU_markPageAsTyped(aURL) {
    585    lazy.PlacesUtils.history.markPageAsTyped(
    586      Services.uriFixup.getFixupURIInfo(aURL).preferredURI
    587    );
    588  },
    589 
    590  /**
    591   * By calling this before visiting an URL, the visit will be associated to a
    592   * TRANSITION_BOOKMARK transition.
    593   * This is used when visiting pages from the bookmarks menu,
    594   * personal toolbar, and bookmarks from within the places organizer.
    595   * If this is not called visits will be marked as TRANSITION_LINK.
    596   *
    597   * @param {string} aURL
    598   *   The URL to mark as TRANSITION_BOOKMARK.
    599   */
    600  markPageAsFollowedBookmark: function PUIU_markPageAsFollowedBookmark(aURL) {
    601    lazy.PlacesUtils.history.markPageAsFollowedBookmark(
    602      Services.uriFixup.getFixupURIInfo(aURL).preferredURI
    603    );
    604  },
    605 
    606  /**
    607   * By calling this before visiting an URL, any visit in frames will be
    608   * associated to a TRANSITION_FRAMED_LINK transition.
    609   * This is actually used to distinguish user-initiated visits in frames
    610   * so automatic visits can be correctly ignored.
    611   *
    612   * @param {string} aURL
    613   *   The URL to mark as TRANSITION_FRAMED_LINK.
    614   */
    615  markPageAsFollowedLink: function PUIU_markPageAsFollowedLink(aURL) {
    616    lazy.PlacesUtils.history.markPageAsFollowedLink(
    617      Services.uriFixup.getFixupURIInfo(aURL).preferredURI
    618    );
    619  },
    620 
    621  /**
    622   * Sets the character-set for a page. The character set will not be saved
    623   * if the window is determined to be a private browsing window.
    624   *
    625   * @param {string|URL|nsIURI} url The URL of the page to set the charset on.
    626   * @param {string} charset character-set value.
    627   * @param {Window} window The window that the charset is being set from.
    628   * @returns {Promise}
    629   */
    630  async setCharsetForPage(url, charset, window) {
    631    if (lazy.PrivateBrowsingUtils.isWindowPrivate(window)) {
    632      return;
    633    }
    634 
    635    // UTF-8 is the default. If we are passed the value then set it to null,
    636    // to ensure any charset is removed from the database.
    637    if (charset.toLowerCase() == "utf-8") {
    638      charset = null;
    639    }
    640 
    641    await lazy.PlacesUtils.history.update({
    642      url,
    643      annotations: new Map([[lazy.PlacesUtils.CHARSET_ANNO, charset]]),
    644    });
    645  },
    646 
    647  /**
    648   * Allows opening of javascript/data URI only if the given node is
    649   * bookmarked (see bug 224521).
    650   *
    651   * @param {object} aURINode
    652   *        a URI node
    653   * @param {Window} aWindow
    654   *        a window on which a potential error alert is shown on.
    655   * @returns {boolean} true if it's safe to open the node in the browser, false otherwise.
    656   */
    657  checkURLSecurity(aURINode, aWindow) {
    658    if (lazy.PlacesUtils.nodeIsBookmark(aURINode)) {
    659      return true;
    660    }
    661 
    662    var uri = Services.io.newURI(aURINode.uri);
    663    if (uri.schemeIs("javascript") || uri.schemeIs("data")) {
    664      const [title, errorStr] =
    665        PlacesUIUtils.promptLocalization.formatValuesSync([
    666          "places-error-title",
    667          "places-load-js-data-url-error",
    668        ]);
    669      Services.prompt.alert(aWindow, title, errorStr);
    670      return false;
    671    }
    672    return true;
    673  },
    674 
    675  /**
    676   * Check whether or not the given node represents a removable entry (either in
    677   * history or in bookmarks).
    678   *
    679   * @param {object} aNode
    680   *        a node, except the root node of a query.
    681   * @returns {boolean} true if the aNode represents a removable entry, false otherwise.
    682   */
    683  canUserRemove(aNode) {
    684    let parentNode = aNode.parent;
    685    if (!parentNode) {
    686      // canUserRemove doesn't accept root nodes.
    687      return false;
    688    }
    689 
    690    // Is it a query pointing to one of the special root folders?
    691    if (lazy.PlacesUtils.nodeIsQuery(parentNode)) {
    692      if (lazy.PlacesUtils.nodeIsFolderOrShortcut(aNode)) {
    693        let guid = lazy.PlacesUtils.getConcreteItemGuid(aNode);
    694        // If the parent folder is not a folder, it must be a query, and so this node
    695        // cannot be removed.
    696        if (lazy.PlacesUtils.isRootItem(guid)) {
    697          return false;
    698        }
    699      } else if (lazy.PlacesUtils.isVirtualLeftPaneItem(aNode.bookmarkGuid)) {
    700        // If the item is a left-pane top-level item, it can't be removed.
    701        return false;
    702      }
    703    }
    704 
    705    // If it's not a bookmark, or it's child of a query, we can remove it.
    706    if (aNode.itemId == -1 || lazy.PlacesUtils.nodeIsQuery(parentNode)) {
    707      return true;
    708    }
    709 
    710    // Otherwise it has to be a child of an editable folder.
    711    return !this.isFolderReadOnly(parentNode);
    712  },
    713 
    714  /**
    715   * DO NOT USE THIS API IN ADDONS. IT IS VERY LIKELY TO CHANGE WHEN THE SWITCH
    716   * TO GUIDS IS COMPLETE (BUG 1071511).
    717   *
    718   * Check whether or not the given Places node points to a folder which
    719   * should not be modified by the user (i.e. its children should be unremovable
    720   * and unmovable, new children should be disallowed, etc).
    721   * These semantics are not inherited, meaning that read-only folder may
    722   * contain editable items (for instance, the places root is read-only, but all
    723   * of its direct children aren't).
    724   *
    725   * You should only pass folder nodes.
    726   *
    727   * @param {object} placesNode
    728   *        any folder result node.
    729   * @throws if placesNode is not a folder result node or views is invalid.
    730   * @returns {boolean} true if placesNode is a read-only folder, false otherwise.
    731   */
    732  isFolderReadOnly(placesNode) {
    733    if (
    734      typeof placesNode != "object" ||
    735      !lazy.PlacesUtils.nodeIsFolderOrShortcut(placesNode)
    736    ) {
    737      throw new Error("invalid value for placesNode");
    738    }
    739 
    740    return (
    741      lazy.PlacesUtils.getConcreteItemGuid(placesNode) ==
    742      lazy.PlacesUtils.bookmarks.rootGuid
    743    );
    744  },
    745 
    746  /**
    747   * @param {Array<object>} aItemsToOpen
    748   *   needs to be an array of objects of the form:
    749   *   {uri: string, isBookmark: boolean}
    750   * @param {object} aEvent
    751   *   The associated event triggering the open.
    752   * @param {Window} aWindow
    753   *   The window associated with the event.
    754   */
    755  openTabset(aItemsToOpen, aEvent, aWindow) {
    756    if (!aItemsToOpen.length) {
    757      return;
    758    }
    759 
    760    let browserWindow = getBrowserWindow(aWindow);
    761    var urls = [];
    762    let isPrivate =
    763      browserWindow && lazy.PrivateBrowsingUtils.isWindowPrivate(browserWindow);
    764    for (let item of aItemsToOpen) {
    765      urls.push(item.uri);
    766      if (isPrivate) {
    767        continue;
    768      }
    769 
    770      if (item.isBookmark) {
    771        this.markPageAsFollowedBookmark(item.uri);
    772      } else {
    773        this.markPageAsTyped(item.uri);
    774      }
    775    }
    776 
    777    // whereToOpenLink doesn't return "window" when there's no browser window
    778    // open (Bug 630255).
    779    var where = browserWindow
    780      ? lazy.BrowserUtils.whereToOpenLink(aEvent, false, true)
    781      : "window";
    782    if (where == "window") {
    783      // There is no browser window open, thus open a new one.
    784      let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
    785      let stringsToLoad = Cc["@mozilla.org/array;1"].createInstance(
    786        Ci.nsIMutableArray
    787      );
    788      urls.forEach(url =>
    789        stringsToLoad.appendElement(lazy.PlacesUtils.toISupportsString(url))
    790      );
    791      args.appendElement(stringsToLoad);
    792 
    793      let features = "chrome,dialog=no,all";
    794      if (isPrivate) {
    795        features += ",private";
    796      }
    797 
    798      browserWindow = Services.ww.openWindow(
    799        aWindow,
    800        AppConstants.BROWSER_CHROME_URL,
    801        null,
    802        features,
    803        args
    804      );
    805      return;
    806    }
    807 
    808    var loadInBackground = where == "tabshifted";
    809    // For consistency, we want all the bookmarks to open in new tabs, instead
    810    // of having one of them replace the currently focused tab.  Hence we call
    811    // loadTabs with aReplace set to false.
    812    browserWindow.gBrowser.loadTabs(urls, {
    813      inBackground: loadInBackground,
    814      replace: false,
    815      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
    816    });
    817  },
    818 
    819  /**
    820   * Loads a selected node's or nodes' URLs in tabs,
    821   * warning the user when lots of URLs are being opened
    822   *
    823   * @param {object | Array} nodeOrNodes
    824   *          Contains the node or nodes that we're opening in tabs
    825   * @param {event} event
    826   *          The DOM mouse/key event with modifier keys set that track the
    827   *          user's preferred destination window or tab.
    828   * @param {object} view
    829   *          The current view that contains the node or nodes selected for
    830   *          opening
    831   */
    832  openMultipleLinksInTabs(nodeOrNodes, event, view) {
    833    let window = view.ownerWindow;
    834    let urlsToOpen = [];
    835 
    836    if (lazy.PlacesUtils.nodeIsContainer(nodeOrNodes)) {
    837      urlsToOpen = lazy.PlacesUtils.getURLsForContainerNode(nodeOrNodes);
    838    } else {
    839      for (var i = 0; i < nodeOrNodes.length; i++) {
    840        // Skip over separators and folders.
    841        if (lazy.PlacesUtils.nodeIsURI(nodeOrNodes[i])) {
    842          urlsToOpen.push({
    843            uri: nodeOrNodes[i].uri,
    844            isBookmark: lazy.PlacesUtils.nodeIsBookmark(nodeOrNodes[i]),
    845          });
    846        }
    847      }
    848    }
    849    if (lazy.OpenInTabsUtils.confirmOpenInTabs(urlsToOpen.length, window)) {
    850      if (window.updateTelemetry) {
    851        window.updateTelemetry(urlsToOpen);
    852      }
    853      this.openTabset(urlsToOpen, event, window);
    854    }
    855  },
    856 
    857  /**
    858   * Loads the node's URL in the appropriate tab or window given the
    859   * user's preference specified by modifier keys tracked by a
    860   * DOM mouse/key event.
    861   *
    862   * @param {object} aNode
    863   *          An uri result node.
    864   * @param {object} aEvent
    865   *          The DOM mouse/key event with modifier keys set that track the
    866   *          user's preferred destination window or tab.
    867   */
    868  openNodeWithEvent: function PUIU_openNodeWithEvent(aNode, aEvent) {
    869    let window = aEvent.target.ownerGlobal;
    870 
    871    let where = lazy.BrowserUtils.whereToOpenLink(aEvent, false, true);
    872    if (this.loadBookmarksInTabs && lazy.PlacesUtils.nodeIsBookmark(aNode)) {
    873      if (where == "current" && !aNode.uri.startsWith("javascript:")) {
    874        where = "tab";
    875      }
    876      let browserWindow = getBrowserWindow(window);
    877      if (where == "tab" && browserWindow?.gBrowser.selectedTab.isEmpty) {
    878        where = "current";
    879      }
    880    }
    881 
    882    this._openNodeIn(aNode, where, window);
    883  },
    884 
    885  /**
    886   * Loads the node's URL in the appropriate tab or window.
    887   * see also URILoadingHelper's openWebLinkIn
    888   *
    889   * @param {object} aNode
    890   *        An uri result node.
    891   * @param {string} aWhere
    892   *        Where to open the URL.
    893   * @param {object} aView
    894   *        The associated view of the node being opened.
    895   * @param {boolean} aPrivate
    896   *        True if the window being opened is private.
    897   */
    898  openNodeIn: function PUIU_openNodeIn(aNode, aWhere, aView, aPrivate) {
    899    let window = aView.ownerWindow;
    900    this._openNodeIn(aNode, aWhere, window, { aPrivate });
    901  },
    902 
    903  _openNodeIn: function PUIU__openNodeIn(
    904    aNode,
    905    aWhere,
    906    aWindow,
    907    { aPrivate = false, userContextId = 0 } = {}
    908  ) {
    909    if (
    910      aNode &&
    911      this.checkURLSecurity(aNode, aWindow) &&
    912      this.isURILike(aNode)
    913    ) {
    914      let isBookmark = lazy.PlacesUtils.nodeIsBookmark(aNode);
    915 
    916      if (!lazy.PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
    917        if (isBookmark) {
    918          this.markPageAsFollowedBookmark(aNode.uri);
    919        } else {
    920          this.markPageAsTyped(aNode.uri);
    921        }
    922      } else {
    923        // This is a targeted fix for bug 1792163, where it was discovered
    924        // that if you open the Library from a Private Browsing window, and then
    925        // use the "Open in New Window" context menu item to open a new window,
    926        // that the window will open under the wrong icon on the Windows taskbar.
    927        aPrivate = true;
    928      }
    929 
    930      const isJavaScriptURL = aNode.uri.startsWith("javascript:");
    931      aWindow.openTrustedLinkIn(aNode.uri, aWhere, {
    932        allowPopups: isJavaScriptURL,
    933        inBackground: this.loadBookmarksInBackground,
    934        allowInheritPrincipal: isJavaScriptURL,
    935        private: aPrivate,
    936        userContextId,
    937      });
    938      if (aWindow.updateTelemetry) {
    939        aWindow.updateTelemetry([aNode]);
    940      }
    941    }
    942  },
    943 
    944  /**
    945   * Determines whether a node represents a URI.
    946   *
    947   * @param {nsINavHistoryResultNode | HTMLElement} aNode
    948   *   A result node.
    949   * @returns {boolean}
    950   *   Whether the node represents a URI.
    951   */
    952  isURILike(aNode) {
    953    if (aNode instanceof Ci.nsINavHistoryResultNode) {
    954      return lazy.PlacesUtils.nodeIsURI(aNode);
    955    }
    956    return !!aNode.uri;
    957  },
    958 
    959  /**
    960   * Helper for guessing scheme from an url string.
    961   * Used to avoid nsIURI overhead in frequently called UI functions. This is not
    962   * supposed be perfect, so use it only for UI purposes.
    963   *
    964   * @param {string} href The url to guess the scheme from.
    965   * @returns {string} guessed scheme for this url string.
    966   */
    967  guessUrlSchemeForUI(href) {
    968    return href.substr(0, href.indexOf(":"));
    969  },
    970 
    971  getBestTitle: function PUIU_getBestTitle(aNode, aDoNotCutTitle) {
    972    var title;
    973    if (!aNode.title && lazy.PlacesUtils.nodeIsURI(aNode)) {
    974      // if node title is empty, try to set the label using host and filename
    975      // Services.io.newURI will throw if aNode.uri is not a valid URI
    976      try {
    977        var uri = Services.io.newURI(aNode.uri);
    978        var host = uri.host;
    979        var fileName = uri.QueryInterface(Ci.nsIURL).fileName;
    980        // if fileName is empty, use path to distinguish labels
    981        if (aDoNotCutTitle) {
    982          title = host + uri.pathQueryRef;
    983        } else {
    984          title =
    985            host +
    986            (fileName
    987              ? (host ? "/" + Services.locale.ellipsis + "/" : "") + fileName
    988              : uri.pathQueryRef);
    989        }
    990      } catch (e) {
    991        // Use (no title) for non-standard URIs (data:, javascript:, ...)
    992        title = "";
    993      }
    994    } else {
    995      title = aNode.title;
    996    }
    997 
    998    return title || this.promptLocalization.formatValueSync("places-no-title");
    999  },
   1000 
   1001  shouldShowTabsFromOtherComputersMenuitem() {
   1002    let weaveOK =
   1003      lazy.Weave.Status.checkSetup() != lazy.CLIENT_NOT_CONFIGURED &&
   1004      lazy.Weave.Svc.PrefBranch.getCharPref("firstSync", "") != "notReady";
   1005    return weaveOK;
   1006  },
   1007 
   1008  /**
   1009   * Helpers for consumers of editBookmarkOverlay which don't have a node as their input.
   1010   *
   1011   * Given a bookmark object for either a url bookmark or a folder, returned by
   1012   * Bookmarks.fetch (see Bookmark.sys.mjs), this creates a node-like object
   1013   * suitable for initialising the edit overlay with it.
   1014   *
   1015   * @param {object} aFetchInfo
   1016   *        a bookmark object returned by Bookmarks.fetch.
   1017   * @returns {Promise<{bookmarkGuid: string, title: string, uri: string, type: nsINavHistoryResultNode.ResultType}>}
   1018   *   A node-like object suitable for initialising editBookmarkOverlay.
   1019   * @throws if aFetchInfo is representing a separator.
   1020   */
   1021  async promiseNodeLikeFromFetchInfo(aFetchInfo) {
   1022    if (aFetchInfo.itemType == lazy.PlacesUtils.bookmarks.TYPE_SEPARATOR) {
   1023      throw new Error("promiseNodeLike doesn't support separators");
   1024    }
   1025 
   1026    let parent = {
   1027      bookmarkGuid: aFetchInfo.parentGuid,
   1028      type: Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
   1029    };
   1030 
   1031    return Object.freeze({
   1032      bookmarkGuid: aFetchInfo.guid,
   1033      title: aFetchInfo.title,
   1034      uri: aFetchInfo.url !== undefined ? aFetchInfo.url.href : "",
   1035 
   1036      get type() {
   1037        if (aFetchInfo.itemType == lazy.PlacesUtils.bookmarks.TYPE_FOLDER) {
   1038          return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER;
   1039        }
   1040 
   1041        if (!this.uri.length) {
   1042          throw new Error("Unexpected item type");
   1043        }
   1044 
   1045        if (/^place:/.test(this.uri)) {
   1046          throw new Error("Place URIs are not supported.");
   1047        }
   1048 
   1049        return Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
   1050      },
   1051 
   1052      get parent() {
   1053        return parent;
   1054      },
   1055    });
   1056  },
   1057 
   1058  /**
   1059   * This function wraps potentially large places transaction operations
   1060   * with batch notifications to the result node, hence switching the views
   1061   * to batch mode. If resultNode is not supplied, the function will
   1062   * pass-through to functionToWrap.
   1063   *
   1064   * @template T
   1065   * @param {nsINavHistoryResult} resultNode The result node to turn on batching.
   1066   * @param {number} itemsBeingChanged The count of items being changed. If the
   1067   *                                    count is lower than a threshold, then
   1068   *                                    batching won't be set.
   1069   * @param {() => T} functionToWrap The function to
   1070   * @returns {Promise<T>} forwards the functionToWrap return value.
   1071   */
   1072  async batchUpdatesForNode(resultNode, itemsBeingChanged, functionToWrap) {
   1073    if (!resultNode) {
   1074      return functionToWrap();
   1075    }
   1076 
   1077    if (itemsBeingChanged > ITEM_CHANGED_BATCH_NOTIFICATION_THRESHOLD) {
   1078      resultNode.onBeginUpdateBatch();
   1079    }
   1080 
   1081    try {
   1082      return await functionToWrap();
   1083    } finally {
   1084      if (itemsBeingChanged > ITEM_CHANGED_BATCH_NOTIFICATION_THRESHOLD) {
   1085        resultNode.onEndUpdateBatch();
   1086      }
   1087    }
   1088  },
   1089 
   1090  /**
   1091   * Processes a set of transfer items that have been dropped or pasted.
   1092   * Batching will be applied where necessary.
   1093   *
   1094   * @param {object[]} items
   1095   *   A list of unwrapped nodes to process.
   1096   * @param {object} insertionPoint
   1097   *   The requested point for insertion.
   1098   * @param {boolean} doCopy
   1099   *   Set to true to copy the items, false will move them if possible.
   1100   * @param {object} view
   1101   *   The view that should be used for batching.
   1102   * @returns {Promise<string[]>}
   1103   *   Returns an empty array when the insertion point is a tag, else returns
   1104   *   an array of copied or moved guids.
   1105   */
   1106  async handleTransferItems(items, insertionPoint, doCopy, view) {
   1107    let transactions;
   1108    let itemsCount;
   1109    if (insertionPoint.isTag) {
   1110      let urls = items.filter(item => "uri" in item).map(item => item.uri);
   1111      itemsCount = urls.length;
   1112      transactions = [
   1113        lazy.PlacesTransactions.Tag({ urls, tag: insertionPoint.tagName }),
   1114      ];
   1115    } else {
   1116      let insertionIndex = await insertionPoint.getIndex();
   1117      itemsCount = items.length;
   1118      transactions = getTransactionsForTransferItems(
   1119        items,
   1120        insertionIndex,
   1121        insertionPoint.guid,
   1122        !doCopy
   1123      );
   1124    }
   1125 
   1126    // Check if we actually have something to add, if we don't it probably wasn't
   1127    // valid, or it was moving to the same location, so just ignore it.
   1128    if (!transactions.length) {
   1129      return [];
   1130    }
   1131 
   1132    let guidsToSelect = await this.batchUpdatesForNode(
   1133      getResultForBatching(view),
   1134      itemsCount,
   1135      async () =>
   1136        lazy.PlacesTransactions.batch(transactions, "handleTransferItems")
   1137    );
   1138 
   1139    // If we're inserting into a tag, we don't get the resulting guids.
   1140    return insertionPoint.isTag ? [] : guidsToSelect.flat();
   1141  },
   1142 
   1143  onSidebarTreeClick(event) {
   1144    // right-clicks are not handled here
   1145    if (event.button == 2) {
   1146      return;
   1147    }
   1148 
   1149    let tree = event.target.parentNode;
   1150    let cell = tree.getCellAt(event.clientX, event.clientY);
   1151    if (cell.row == -1 || cell.childElt == "twisty") {
   1152      return;
   1153    }
   1154 
   1155    // getCoordsForCellItem returns the x coordinate in logical coordinates
   1156    // (i.e., starting from the left and right sides in LTR and RTL modes,
   1157    // respectively.)  Therefore, we make sure to exclude the blank area
   1158    // before the tree item icon (that is, to the left or right of it in
   1159    // LTR and RTL modes, respectively) from the click target area.
   1160    let win = tree.ownerGlobal;
   1161    let rect = tree.getCoordsForCellItem(cell.row, cell.col, "image");
   1162    let isRTL = win.getComputedStyle(tree).direction == "rtl";
   1163    let mouseInGutter = isRTL ? event.clientX > rect.x : event.clientX < rect.x;
   1164 
   1165    let metaKey =
   1166      AppConstants.platform === "macosx" ? event.metaKey : event.ctrlKey;
   1167    let modifKey = metaKey || event.shiftKey;
   1168    let isContainer = tree.view.isContainer(cell.row);
   1169    let openInTabs =
   1170      isContainer &&
   1171      (event.button == 1 || (event.button == 0 && modifKey)) &&
   1172      lazy.PlacesUtils.hasChildURIs(tree.view.nodeForTreeIndex(cell.row));
   1173 
   1174    if (event.button == 0 && isContainer && !openInTabs) {
   1175      tree.view.toggleOpenState(cell.row);
   1176    } else if (
   1177      !mouseInGutter &&
   1178      openInTabs &&
   1179      event.originalTarget.localName == "treechildren"
   1180    ) {
   1181      tree.view.selection.select(cell.row);
   1182      this.openMultipleLinksInTabs(tree.selectedNode, event, tree);
   1183    } else if (
   1184      !mouseInGutter &&
   1185      !isContainer &&
   1186      event.originalTarget.localName == "treechildren"
   1187    ) {
   1188      // Clear all other selection since we're loading a link now. We must
   1189      // do this *before* attempting to load the link since openURL uses
   1190      // selection as an indication of which link to load.
   1191      tree.view.selection.select(cell.row);
   1192      this.openNodeWithEvent(tree.selectedNode, event);
   1193    }
   1194  },
   1195 
   1196  onSidebarTreeKeyPress(event) {
   1197    let node = event.target.selectedNode;
   1198    if (node) {
   1199      if (event.keyCode == event.DOM_VK_RETURN) {
   1200        PlacesUIUtils.openNodeWithEvent(node, event);
   1201      }
   1202    }
   1203  },
   1204 
   1205  /**
   1206   * The following function displays the URL of a node that is being
   1207   * hovered over.
   1208   *
   1209   * @param {object} event
   1210   *   The event that triggered the hover.
   1211   */
   1212  onSidebarTreeMouseMove(event) {
   1213    let treechildren = event.target;
   1214    if (treechildren.localName != "treechildren") {
   1215      return;
   1216    }
   1217 
   1218    let tree = treechildren.parentNode;
   1219    let cell = tree.getCellAt(event.clientX, event.clientY);
   1220 
   1221    // cell.row is -1 when the mouse is hovering an empty area within the tree.
   1222    // To avoid showing a URL from a previously hovered node for a currently
   1223    // hovered non-url node, we must clear the moused-over URL in these cases.
   1224    if (cell.row != -1) {
   1225      let node = tree.view.nodeForTreeIndex(cell.row);
   1226      if (lazy.PlacesUtils.nodeIsURI(node)) {
   1227        this.setMouseoverURL(node.uri, tree.ownerGlobal);
   1228        return;
   1229      }
   1230    }
   1231    this.setMouseoverURL("", tree.ownerGlobal);
   1232  },
   1233 
   1234  setMouseoverURL(url, win) {
   1235    // When the browser window is closed with an open sidebar, the sidebar
   1236    // unload event happens after the browser's one.  In this case
   1237    // top.XULBrowserWindow has been nullified already.
   1238    if (win.top.XULBrowserWindow) {
   1239      win.top.XULBrowserWindow.setOverLink(url);
   1240    }
   1241  },
   1242 
   1243  /**
   1244   * Uncollapses PersonalToolbar if its collapsed status is not
   1245   * persisted, and user customized it or changed default bookmarks.
   1246   *
   1247   * If the user does not have a persisted value for the toolbar's
   1248   * "collapsed" attribute, try to determine whether it's customized.
   1249   *
   1250   * @param {boolean} aForceVisible Set to true to ignore if the user had
   1251   * previously collapsed the toolbar manually.
   1252   */
   1253  NUM_TOOLBAR_BOOKMARKS_TO_UNHIDE: 3,
   1254  async maybeToggleBookmarkToolbarVisibility(aForceVisible = false) {
   1255    const BROWSER_DOCURL = AppConstants.BROWSER_CHROME_URL;
   1256    let xulStore = Services.xulStore;
   1257 
   1258    if (
   1259      aForceVisible ||
   1260      !xulStore.hasValue(BROWSER_DOCURL, "PersonalToolbar", "collapsed")
   1261    ) {
   1262      function uncollapseToolbar() {
   1263        Services.obs.notifyObservers(
   1264          null,
   1265          "browser-set-toolbar-visibility",
   1266          JSON.stringify([lazy.CustomizableUI.AREA_BOOKMARKS, "true"])
   1267        );
   1268      }
   1269      // We consider the toolbar customized if it has more than
   1270      // NUM_TOOLBAR_BOOKMARKS_TO_UNHIDE children, or if it has a persisted
   1271      // currentset value.
   1272      let toolbarIsCustomized = xulStore.hasValue(
   1273        BROWSER_DOCURL,
   1274        "PersonalToolbar",
   1275        "currentset"
   1276      );
   1277      if (aForceVisible || toolbarIsCustomized) {
   1278        uncollapseToolbar();
   1279        return;
   1280      }
   1281 
   1282      let numBookmarksOnToolbar = (
   1283        await lazy.PlacesUtils.bookmarks.fetch(
   1284          lazy.PlacesUtils.bookmarks.toolbarGuid
   1285        )
   1286      ).childCount;
   1287      if (numBookmarksOnToolbar > this.NUM_TOOLBAR_BOOKMARKS_TO_UNHIDE) {
   1288        uncollapseToolbar();
   1289      }
   1290    }
   1291  },
   1292 
   1293  /**
   1294   * Determines whether the given "placesContext" menu item would open a link
   1295   * under some special conditions, but those special conditions cannot be met.
   1296   *
   1297   * @param {Element} item The menu or menu item to decide for.
   1298   *
   1299   * @returns {boolean} Whether the item is an "open" item that should be
   1300   *   hidden.
   1301   */
   1302  shouldHideOpenMenuItem(item) {
   1303    if (
   1304      item.hasAttribute("hide-if-disabled-private-browsing") &&
   1305      !lazy.PrivateBrowsingUtils.enabled
   1306    ) {
   1307      return true;
   1308    }
   1309 
   1310    if (
   1311      item.hasAttribute("hide-if-private-browsing") &&
   1312      lazy.PrivateBrowsingUtils.isWindowPrivate(item.ownerGlobal)
   1313    ) {
   1314      return true;
   1315    }
   1316 
   1317    if (
   1318      item.hasAttribute("hide-if-usercontext-disabled") &&
   1319      !Services.prefs.getBoolPref("privacy.userContext.enabled", false)
   1320    ) {
   1321      return true;
   1322    }
   1323 
   1324    return false;
   1325  },
   1326 
   1327  async managedPlacesContextShowing(event) {
   1328    let menupopup = event.target;
   1329    let document = menupopup.ownerDocument;
   1330    let window = menupopup.ownerGlobal;
   1331    // We need to populate the submenus in order to have information
   1332    // to show the context menu.
   1333    if (
   1334      menupopup.triggerNode.id == "managed-bookmarks" &&
   1335      !menupopup.triggerNode.menupopup.hasAttribute("hasbeenopened")
   1336    ) {
   1337      await window.PlacesToolbarHelper.populateManagedBookmarks(
   1338        menupopup.triggerNode.menupopup
   1339      );
   1340    }
   1341    // Hide everything. We'll unhide the things we need.
   1342    Array.from(menupopup.children).forEach(function (child) {
   1343      child.hidden = true;
   1344    });
   1345    // Store triggerNode in controller for checking if commands are enabled
   1346    this.managedBookmarksController.triggerNode = menupopup.triggerNode;
   1347    // Container in this context means a folder.
   1348    let isFolder = menupopup.triggerNode.hasAttribute("container");
   1349    if (isFolder) {
   1350      // Disable the openContainerInTabs menuitem if there
   1351      // are no children of the menu that have links.
   1352      let openContainerInTabs_menuitem = document.getElementById(
   1353        "placesContext_openContainer:tabs"
   1354      );
   1355      let menuitems = menupopup.triggerNode.menupopup.children;
   1356      let openContainerInTabs = Array.from(menuitems).some(
   1357        menuitem => menuitem.link
   1358      );
   1359      openContainerInTabs_menuitem.disabled = !openContainerInTabs;
   1360      openContainerInTabs_menuitem.hidden = false;
   1361    } else {
   1362      for (let id of [
   1363        "placesContext_open:newtab",
   1364        "placesContext_open:newcontainertab",
   1365        "placesContext_open:newwindow",
   1366        "placesContext_open:newprivatewindow",
   1367      ]) {
   1368        let item = document.getElementById(id);
   1369        item.hidden = this.shouldHideOpenMenuItem(item);
   1370      }
   1371      for (let id of ["placesContext_openSeparator", "placesContext_copy"]) {
   1372        document.getElementById(id).hidden = false;
   1373      }
   1374    }
   1375 
   1376    event.target.ownerGlobal.updateCommands("places");
   1377  },
   1378 
   1379  placesContextShowing(event) {
   1380    let menupopup = /** @type {XULPopupElement} */ (event.target);
   1381    if (
   1382      !["placesContext", "sidebar-history-context-menu"].includes(menupopup.id)
   1383    ) {
   1384      // Ignore any popupshowing events from submenus
   1385      return;
   1386    }
   1387 
   1388    if (menupopup.id == "sidebar-history-context-menu") {
   1389      PlacesUIUtils.lastContextMenuTriggerNode =
   1390        menupopup.triggerNode.triggerNode;
   1391      return;
   1392    }
   1393 
   1394    PlacesUIUtils.lastContextMenuTriggerNode = menupopup.triggerNode;
   1395 
   1396    if (Services.prefs.getBoolPref("browser.tabs.loadBookmarksInTabs", false)) {
   1397      menupopup.ownerDocument
   1398        .getElementById("placesContext_open")
   1399        .removeAttribute("default");
   1400      menupopup.ownerDocument
   1401        .getElementById("placesContext_open:newtab")
   1402        .setAttribute("default", "true");
   1403      // else clause ensures correct behavior if pref is repeatedly toggled
   1404    } else {
   1405      menupopup.ownerDocument
   1406        .getElementById("placesContext_open:newtab")
   1407        .removeAttribute("default");
   1408      menupopup.ownerDocument
   1409        .getElementById("placesContext_open")
   1410        .setAttribute("default", "true");
   1411    }
   1412 
   1413    let isManaged = !!menupopup.triggerNode.closest("#managed-bookmarks");
   1414    if (isManaged) {
   1415      this.managedPlacesContextShowing(event);
   1416      return;
   1417    }
   1418    menupopup._view = this.getViewForNode(menupopup.triggerNode);
   1419    if (!menupopup._view) {
   1420      // This can happen if we try to invoke the context menu on
   1421      // an uninitialized places toolbar. Just bail out:
   1422      event.preventDefault();
   1423      return;
   1424    }
   1425    if (!this.openInTabClosesMenu) {
   1426      menupopup.ownerDocument
   1427        .getElementById("placesContext_open:newtab")
   1428        .setAttribute("closemenu", "single");
   1429    }
   1430    if (!menupopup._view.buildContextMenu(menupopup)) {
   1431      event.preventDefault();
   1432    }
   1433  },
   1434 
   1435  placesContextHiding(event) {
   1436    let menupopup = event.target;
   1437    if (menupopup._view) {
   1438      menupopup._view.destroyContextMenu();
   1439    }
   1440 
   1441    if (
   1442      [
   1443        "sidebar-history-context-menu",
   1444        "placesContext",
   1445        "sidebar-synced-tabs-context-menu",
   1446      ].includes(menupopup.id)
   1447    ) {
   1448      PlacesUIUtils.lastContextMenuTriggerNode = null;
   1449      PlacesUIUtils.lastContextMenuCommand = null;
   1450    }
   1451  },
   1452 
   1453  createContainerTabMenu(event) {
   1454    let window = event.target.ownerGlobal;
   1455    return window.createUserContextMenu(event, { isContextMenu: true });
   1456  },
   1457 
   1458  openInContainerTab(event) {
   1459    PlacesUIUtils.lastContextMenuCommand = "placesCmd_open:newcontainertab";
   1460    let userContextId = parseInt(
   1461      event.target.getAttribute("data-usercontextid")
   1462    );
   1463    let triggerNode = this.lastContextMenuTriggerNode;
   1464    let isManaged = !!triggerNode?.closest("#managed-bookmarks");
   1465    if (isManaged) {
   1466      let window = triggerNode.ownerGlobal;
   1467      window.openTrustedLinkIn(triggerNode.link, "tab", { userContextId });
   1468      return;
   1469    }
   1470    let view = this.getViewForNode(triggerNode);
   1471    this._openNodeIn(
   1472      view?.selectedNode || triggerNode,
   1473      "tab",
   1474      view?.ownerWindow || triggerNode.ownerGlobal.top,
   1475      {
   1476        userContextId,
   1477      }
   1478    );
   1479  },
   1480 
   1481  openSelectionInTabs(event) {
   1482    let isManaged =
   1483      !!event.target.parentNode.triggerNode.closest("#managed-bookmarks");
   1484    let controller;
   1485    if (isManaged) {
   1486      controller = this.managedBookmarksController;
   1487    } else {
   1488      controller = PlacesUIUtils.getViewForNode(
   1489        PlacesUIUtils.lastContextMenuTriggerNode
   1490      ).controller;
   1491    }
   1492    controller.openSelectionInTabs(event);
   1493  },
   1494 
   1495  managedBookmarksController: {
   1496    triggerNode: null,
   1497 
   1498    openSelectionInTabs(event) {
   1499      let window = event.target.ownerGlobal;
   1500      let menuitems = event.target.parentNode.triggerNode.menupopup.children;
   1501      let items = [];
   1502      for (let i = 0; i < menuitems.length; i++) {
   1503        if (menuitems[i].link) {
   1504          let item = {};
   1505          item.uri = menuitems[i].link;
   1506          item.isBookmark = true;
   1507          items.push(item);
   1508        }
   1509      }
   1510      PlacesUIUtils.openTabset(items, event, window);
   1511    },
   1512 
   1513    isCommandEnabled(command) {
   1514      switch (command) {
   1515        case "placesCmd_copy":
   1516        case "placesCmd_open:window":
   1517        case "placesCmd_open:privatewindow":
   1518        case "placesCmd_open:tab": {
   1519          return true;
   1520        }
   1521      }
   1522      return false;
   1523    },
   1524 
   1525    doCommand(command) {
   1526      let window = this.triggerNode.ownerGlobal;
   1527      switch (command) {
   1528        case "placesCmd_copy": {
   1529          lazy.BrowserUtils.copyLink(
   1530            this.triggerNode.link,
   1531            this.triggerNode.label
   1532          );
   1533          break;
   1534        }
   1535        case "placesCmd_open:privatewindow":
   1536          window.openTrustedLinkIn(this.triggerNode.link, "window", {
   1537            private: true,
   1538          });
   1539          break;
   1540        case "placesCmd_open:window":
   1541          window.openTrustedLinkIn(this.triggerNode.link, "window", {
   1542            private: false,
   1543          });
   1544          break;
   1545        case "placesCmd_open:tab": {
   1546          window.openTrustedLinkIn(this.triggerNode.link, "tab");
   1547        }
   1548      }
   1549    },
   1550  },
   1551 
   1552  async maybeAddImportButton() {
   1553    if (!Services.policies.isAllowed("profileImport")) {
   1554      return;
   1555    }
   1556 
   1557    let numberOfBookmarks = await lazy.PlacesUtils.withConnectionWrapper(
   1558      "PlacesUIUtils: maybeAddImportButton",
   1559      async db => {
   1560        let rows = await db.execute(
   1561          `SELECT COUNT(*) as n FROM moz_bookmarks b
   1562           JOIN moz_bookmarks p ON p.id = b.parent
   1563           WHERE p.guid = :guid`,
   1564          { guid: lazy.PlacesUtils.bookmarks.toolbarGuid }
   1565        );
   1566        return rows[0].getResultByName("n");
   1567      }
   1568    ).catch(e => {
   1569      // We want to report errors, but we still want to add the button then:
   1570      console.error(e);
   1571      return 0;
   1572    });
   1573 
   1574    if (numberOfBookmarks < 3) {
   1575      lazy.CustomizableUI.addWidgetToArea(
   1576        "import-button",
   1577        lazy.CustomizableUI.AREA_BOOKMARKS,
   1578        0
   1579      );
   1580      Services.prefs.setBoolPref("browser.bookmarks.addedImportButton", true);
   1581      this.removeImportButtonWhenImportSucceeds();
   1582    }
   1583  },
   1584 
   1585  removeImportButtonWhenImportSucceeds() {
   1586    // If the user (re)moved the button, clear the pref and stop worrying about
   1587    // moving the item.
   1588    let placement = lazy.CustomizableUI.getPlacementOfWidget("import-button");
   1589    if (placement?.area != lazy.CustomizableUI.AREA_BOOKMARKS) {
   1590      Services.prefs.clearUserPref("browser.bookmarks.addedImportButton");
   1591      return;
   1592    }
   1593    // Otherwise, wait for a successful migration:
   1594    let obs = (subject, topic, data) => {
   1595      if (
   1596        data == lazy.MigrationUtils.resourceTypes.BOOKMARKS &&
   1597        lazy.MigrationUtils.getImportedCount("bookmarks") > 0
   1598      ) {
   1599        lazy.CustomizableUI.removeWidgetFromArea("import-button");
   1600        Services.prefs.clearUserPref("browser.bookmarks.addedImportButton");
   1601        Services.obs.removeObserver(obs, "Migration:ItemAfterMigrate");
   1602        Services.obs.removeObserver(obs, "Migration:ItemError");
   1603      }
   1604    };
   1605    Services.obs.addObserver(obs, "Migration:ItemAfterMigrate");
   1606    Services.obs.addObserver(obs, "Migration:ItemError");
   1607  },
   1608 
   1609  /**
   1610   * Tries to initiate a speculative connection to a given url. This is not
   1611   * infallible, if a speculative connection cannot be initialized, it will be a
   1612   * no-op.
   1613   *
   1614   * @param {string} url entity to initiate
   1615   *        a speculative connection for.
   1616   * @param {Window} window the window from where the connection is initialized.
   1617   */
   1618  setupSpeculativeConnection(url, window) {
   1619    if (
   1620      !Services.prefs.getBoolPref(
   1621        "browser.places.speculativeConnect.enabled",
   1622        true
   1623      )
   1624    ) {
   1625      return;
   1626    }
   1627    if (!url.startsWith("http")) {
   1628      return;
   1629    }
   1630    try {
   1631      let uri = Services.io.newURI(url);
   1632      Services.io.speculativeConnect(
   1633        uri,
   1634        window.gBrowser.contentPrincipal,
   1635        null,
   1636        false
   1637      );
   1638    } catch (ex) {
   1639      // Can't setup speculative connection for this url, just ignore it.
   1640    }
   1641  },
   1642 
   1643  /**
   1644   * Sets up a speculative connection to the target of a
   1645   * clicked places DOM node on left and middle click.
   1646   *
   1647   * @param {MouseEvent} event the mousedown event.
   1648   */
   1649  maybeSpeculativeConnectOnMouseDown(event) {
   1650    if (
   1651      event.type == "mousedown" &&
   1652      event.target._placesNode?.uri &&
   1653      event.button != 2
   1654    ) {
   1655      PlacesUIUtils.setupSpeculativeConnection(
   1656        event.target._placesNode.uri,
   1657        event.target.ownerGlobal
   1658      );
   1659    }
   1660  },
   1661 
   1662  /**
   1663   * Generates a cached-favicon: link for an icon URL, that will allow to fetch
   1664   * the icon from the local favicons cache, rather than from the network.
   1665   * If the icon URL is invalid, fallbacks to the default favicon URL.
   1666   *
   1667   * @param {string} icon The url of the icon to load from local cache.
   1668   * @returns {string} a "cached-favicon:" prefixed URL, unless the original
   1669   *   URL protocol refers to a local resource, then it will just pass-through
   1670   *   unchanged.
   1671   */
   1672  getImageURL(icon) {
   1673    // don't initiate a connection just to fetch a favicon (see bug 467828)
   1674    try {
   1675      return lazy.PlacesUtils.favicons.getFaviconLinkForIcon(
   1676        Services.io.newURI(icon)
   1677      ).spec;
   1678    } catch (ex) {}
   1679    return lazy.PlacesUtils.favicons.defaultFavicon.spec;
   1680  },
   1681 
   1682  /**
   1683   * Determines the string indexes where titles differ from similar titles (where
   1684   * the first n characters are the same) in the provided list of items, and
   1685   * adds that into the item.
   1686   *
   1687   * This assumes the titles will be displayed along the lines of
   1688   * `Start of title ... place where differs` the index would be reference
   1689   * the `p` here.
   1690   *
   1691   * @param {object[]} candidates
   1692   *   An array of candidates to modify. The candidates should have a `title`
   1693   *   property which should be a string or null.
   1694   *   The order of the array does not matter. The objects are modified
   1695   *   in-place.
   1696   *   If a difference to other similar titles is found then a
   1697   *   `titleDifferentIndex` property will be inserted into all similar
   1698   *   candidates with the index of the start of the difference.
   1699   */
   1700  insertTitleStartDiffs(candidates) {
   1701    function findStartDifference(a, b) {
   1702      let i;
   1703      // We already know the start is the same, so skip that part.
   1704      for (i = PlacesUIUtils.similarTitlesMinChars; i < a.length; i++) {
   1705        if (a[i] != b[i]) {
   1706          return i;
   1707        }
   1708      }
   1709      if (b.length > i) {
   1710        return i;
   1711      }
   1712      // They are the same.
   1713      return -1;
   1714    }
   1715 
   1716    let longTitles = new Map();
   1717 
   1718    for (let candidate of candidates) {
   1719      // Title is too short for us to care about, simply continue.
   1720      if (
   1721        !candidate.title ||
   1722        candidate.title.length < this.similarTitlesMinChars
   1723      ) {
   1724        continue;
   1725      }
   1726      let titleBeginning = candidate.title.slice(0, this.similarTitlesMinChars);
   1727      let matches = longTitles.get(titleBeginning);
   1728      if (matches) {
   1729        for (let match of matches) {
   1730          let startDiff = findStartDifference(candidate.title, match.title);
   1731          if (startDiff > 0) {
   1732            candidate.titleDifferentIndex = startDiff;
   1733            // If we have an existing difference index for the match, move
   1734            // it forward if this one is earlier in the string.
   1735            if (
   1736              !("titleDifferentIndex" in match) ||
   1737              match.titleDifferentIndex > startDiff
   1738            ) {
   1739              match.titleDifferentIndex = startDiff;
   1740            }
   1741          }
   1742        }
   1743 
   1744        matches.push(candidate);
   1745      } else {
   1746        longTitles.set(titleBeginning, [candidate]);
   1747      }
   1748    }
   1749  },
   1750 };
   1751 
   1752 /**
   1753 * Promise used by the toolbar view browser-places to determine whether we
   1754 * can start loading its content (which involves IO, and so is postponed
   1755 * during startup).
   1756 */
   1757 PlacesUIUtils.canLoadToolbarContentPromise = new Promise(resolve => {
   1758  PlacesUIUtils.unblockToolbars = resolve;
   1759 });
   1760 
   1761 // These are lazy getters to avoid importing PlacesUtils immediately.
   1762 ChromeUtils.defineLazyGetter(PlacesUIUtils, "PLACES_FLAVORS", () => {
   1763  return [
   1764    lazy.PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
   1765    lazy.PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
   1766    lazy.PlacesUtils.TYPE_X_MOZ_PLACE,
   1767  ];
   1768 });
   1769 ChromeUtils.defineLazyGetter(PlacesUIUtils, "URI_FLAVORS", () => {
   1770  return [
   1771    lazy.PlacesUtils.TYPE_X_MOZ_URL,
   1772    TAB_DROP_TYPE,
   1773    lazy.PlacesUtils.TYPE_PLAINTEXT,
   1774  ];
   1775 });
   1776 ChromeUtils.defineLazyGetter(PlacesUIUtils, "SUPPORTED_FLAVORS", () => {
   1777  return [
   1778    ...PlacesUIUtils.PLACES_FLAVORS,
   1779    ...PlacesUIUtils.URI_FLAVORS,
   1780    "application/x-torbrowser-opaque",
   1781  ];
   1782 });
   1783 
   1784 ChromeUtils.defineLazyGetter(PlacesUIUtils, "promptLocalization", () => {
   1785  return new Localization(
   1786    ["browser/placesPrompts.ftl", "branding/brand.ftl"],
   1787    true
   1788  );
   1789 });
   1790 
   1791 XPCOMUtils.defineLazyPreferenceGetter(
   1792  PlacesUIUtils,
   1793  "similarTitlesMinChars",
   1794  "browser.places.similarTitlesMinChars",
   1795  20
   1796 );
   1797 XPCOMUtils.defineLazyPreferenceGetter(
   1798  PlacesUIUtils,
   1799  "loadBookmarksInBackground",
   1800  "browser.tabs.loadBookmarksInBackground",
   1801  false
   1802 );
   1803 XPCOMUtils.defineLazyPreferenceGetter(
   1804  PlacesUIUtils,
   1805  "loadBookmarksInTabs",
   1806  "browser.tabs.loadBookmarksInTabs",
   1807  false
   1808 );
   1809 XPCOMUtils.defineLazyPreferenceGetter(
   1810  PlacesUIUtils,
   1811  "openInTabClosesMenu",
   1812  "browser.bookmarks.openInTabClosesMenu",
   1813  false
   1814 );
   1815 XPCOMUtils.defineLazyPreferenceGetter(
   1816  PlacesUIUtils,
   1817  "maxRecentFolders",
   1818  "browser.bookmarks.editDialog.maxRecentFolders",
   1819  7
   1820 );
   1821 
   1822 XPCOMUtils.defineLazyPreferenceGetter(
   1823  PlacesUIUtils,
   1824  "defaultParentGuid",
   1825  "browser.bookmarks.defaultLocation",
   1826  "", // Avoid eagerly loading PlacesUtils.
   1827  null,
   1828  async prefValue => {
   1829    if (!prefValue) {
   1830      return lazy.PlacesUtils.bookmarks.toolbarGuid;
   1831    }
   1832    if (["toolbar", "menu", "unfiled"].includes(prefValue)) {
   1833      return lazy.PlacesUtils.bookmarks[prefValue + "Guid"];
   1834    }
   1835 
   1836    try {
   1837      return await lazy.PlacesUtils.bookmarks
   1838        .fetch({ guid: prefValue })
   1839        .then(bm => bm.guid);
   1840    } catch (ex) {
   1841      // The guid may have an invalid format.
   1842      return lazy.PlacesUtils.bookmarks.toolbarGuid;
   1843    }
   1844  }
   1845 );
   1846 
   1847 /**
   1848 * Determines if an unwrapped node can be moved.
   1849 *
   1850 * @param {object} unwrappedNode
   1851 *        A node unwrapped by PlacesUtils.unwrapNodes().
   1852 * @returns {boolean} True if the node can be moved, false otherwise.
   1853 */
   1854 function canMoveUnwrappedNode(unwrappedNode) {
   1855  if (
   1856    (unwrappedNode.concreteGuid &&
   1857      lazy.PlacesUtils.isRootItem(unwrappedNode.concreteGuid)) ||
   1858    (unwrappedNode.guid && lazy.PlacesUtils.isRootItem(unwrappedNode.guid))
   1859  ) {
   1860    return false;
   1861  }
   1862 
   1863  let parentGuid = unwrappedNode.parentGuid;
   1864  if (parentGuid == lazy.PlacesUtils.bookmarks.rootGuid) {
   1865    return false;
   1866  }
   1867 
   1868  return true;
   1869 }
   1870 
   1871 /**
   1872 * This gets the most appropriate item for using for batching. In the case of multiple
   1873 * views being related, the method returns the most expensive result to batch.
   1874 * For example, if it detects the left-hand library pane, then it will look for
   1875 * and return the reference to the right-hand pane.
   1876 *
   1877 * @param {object} viewOrElement The item to check.
   1878 * @returns {object} Will return the best result node to batch, or null
   1879 *                  if one could not be found.
   1880 */
   1881 function getResultForBatching(viewOrElement) {
   1882  if (
   1883    viewOrElement &&
   1884    Element.isInstance(viewOrElement) &&
   1885    viewOrElement.id === "placesList"
   1886  ) {
   1887    // Note: fall back to the existing item if we can't find the right-hane pane.
   1888    viewOrElement =
   1889      viewOrElement.ownerDocument.getElementById("placeContent") ||
   1890      viewOrElement;
   1891  }
   1892 
   1893  if (viewOrElement && viewOrElement.result) {
   1894    return viewOrElement.result;
   1895  }
   1896 
   1897  return null;
   1898 }
   1899 
   1900 /**
   1901 * Processes a set of transfer items and returns transactions to insert or
   1902 * move them.
   1903 *
   1904 * @param {Array} items A list of unwrapped nodes to get transactions for.
   1905 * @param {number} insertionIndex The requested index for insertion.
   1906 * @param {string} insertionParentGuid The guid of the parent folder to insert
   1907 *                                     or move the items to.
   1908 * @param {boolean} doMove Set to true to MOVE the items if possible, false will
   1909 *                         copy them.
   1910 * @returns {Array} Returns an array of created PlacesTransactions.
   1911 */
   1912 function getTransactionsForTransferItems(
   1913  items,
   1914  insertionIndex,
   1915  insertionParentGuid,
   1916  doMove
   1917 ) {
   1918  let canMove = true;
   1919  for (let item of items) {
   1920    if (!PlacesUIUtils.SUPPORTED_FLAVORS.includes(item.type)) {
   1921      throw new Error(`Unsupported '${item.type}' data type`);
   1922    }
   1923 
   1924    // Work out if this is data from the same app session we're running in.
   1925    if (
   1926      !("instanceId" in item) ||
   1927      item.instanceId != lazy.PlacesUtils.instanceId
   1928    ) {
   1929      if (item.type == lazy.PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
   1930        throw new Error(
   1931          "Can't copy a container from a legacy-transactions build"
   1932        );
   1933      }
   1934      // Only log if this is one of "our" types as external items, e.g. drag from
   1935      // url bar to toolbar, shouldn't complain.
   1936      if (PlacesUIUtils.PLACES_FLAVORS.includes(item.type)) {
   1937        console.error(
   1938          "Tried to move an unmovable Places " +
   1939            "node, reverting to a copy operation."
   1940        );
   1941      }
   1942 
   1943      // We can never move from an external copy.
   1944      canMove = false;
   1945    }
   1946 
   1947    if (doMove && canMove) {
   1948      canMove = canMoveUnwrappedNode(item);
   1949    }
   1950  }
   1951 
   1952  if (doMove && !canMove) {
   1953    doMove = false;
   1954  }
   1955 
   1956  if (doMove) {
   1957    // Move is simple, we pass the transaction a list of GUIDs and where to move
   1958    // them to.
   1959    return [
   1960      lazy.PlacesTransactions.Move({
   1961        guids: items.map(item => item.itemGuid),
   1962        newParentGuid: insertionParentGuid,
   1963        newIndex: insertionIndex,
   1964      }),
   1965    ];
   1966  }
   1967 
   1968  return getTransactionsForCopy(items, insertionIndex, insertionParentGuid);
   1969 }
   1970 
   1971 /**
   1972 * Processes a set of transfer items and returns an array of transactions.
   1973 *
   1974 * @param {Array} items A list of unwrapped nodes to get transactions for.
   1975 * @param {number} insertionIndex The requested index for insertion.
   1976 * @param {string} insertionParentGuid The guid of the parent folder to insert
   1977 *                                     or move the items to.
   1978 * @returns {Array} Returns an array of created PlacesTransactions.
   1979 */
   1980 function getTransactionsForCopy(items, insertionIndex, insertionParentGuid) {
   1981  let transactions = [];
   1982  let index = insertionIndex;
   1983 
   1984  for (let item of items) {
   1985    let transaction;
   1986    let guid = item.itemGuid;
   1987 
   1988    if (
   1989      PlacesUIUtils.PLACES_FLAVORS.includes(item.type) &&
   1990      // For anything that is comming from within this session, we do a
   1991      // direct copy, otherwise we fallback and form a new item below.
   1992      "instanceId" in item &&
   1993      item.instanceId == lazy.PlacesUtils.instanceId &&
   1994      // If the Item doesn't have a guid, this could be a virtual tag query or
   1995      // other item, so fallback to inserting a new bookmark with the URI.
   1996      guid &&
   1997      // For virtual root items, we fallback to creating a new bookmark, as
   1998      // we want a shortcut to be created, not a full tree copy.
   1999      !lazy.PlacesUtils.bookmarks.isVirtualRootItem(guid) &&
   2000      !lazy.PlacesUtils.isVirtualLeftPaneItem(guid)
   2001    ) {
   2002      transaction = lazy.PlacesTransactions.Copy({
   2003        guid,
   2004        newIndex: index,
   2005        newParentGuid: insertionParentGuid,
   2006      });
   2007    } else if (item.type == lazy.PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) {
   2008      transaction = lazy.PlacesTransactions.NewSeparator({
   2009        index,
   2010        parentGuid: insertionParentGuid,
   2011      });
   2012    } else {
   2013      let title =
   2014        item.type != lazy.PlacesUtils.TYPE_PLAINTEXT ? item.title : item.uri;
   2015      transaction = lazy.PlacesTransactions.NewBookmark({
   2016        index,
   2017        parentGuid: insertionParentGuid,
   2018        title,
   2019        url: item.uri,
   2020      });
   2021    }
   2022 
   2023    transactions.push(transaction);
   2024 
   2025    if (index != -1) {
   2026      index++;
   2027    }
   2028  }
   2029  return transactions;
   2030 }
   2031 
   2032 function getBrowserWindow(aWindow) {
   2033  // Prefer the caller window if it's a browser window, otherwise use
   2034  // the top browser window.
   2035  return aWindow &&
   2036    aWindow.document.documentElement.getAttribute("windowtype") ==
   2037      "navigator:browser"
   2038    ? aWindow
   2039    : lazy.BrowserWindowTracker.getTopWindow();
   2040 }